graphql_relay_identifier 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 56aa2e3f2f181b151ea6e740ccb5c5aec1668585b9e6d6bff687af76bc7ae29c
4
+ data.tar.gz: 67aec04391bd3f0513df0e26a47f84430555cb2af07620422a6e03d2b3703007
5
+ SHA512:
6
+ metadata.gz: b721ad621ccb01c5409d5a7c38464c5ff8eefbea2b4715e6e5161a4b8934e209350f598f6b7811875b6e07632f43930631a4e1d57289ecceccc205fdf5e63bb5
7
+ data.tar.gz: 7aa148816743945383413b9de8113c743cf70c6be06a7ddc873c3349e3bc5afae4567615815ff256d623631a8963956d11493a0631971204bf2a2771dc81660a
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2025-06-19
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Mark Faga
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # GraphQL::Relay::Identifier
2
+
3
+ This gem provides an implementation the Relay Global Object Identification specification in Ruby,
4
+ ensuring compatibility with GraphQL Federation. It allows you to define and resolve global
5
+ identifiers for your GraphQL objects, making it easier to work with Relay and GraphQL Federation in
6
+ your applications.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's `Gemfile`:
11
+
12
+ ```ruby
13
+ gem 'graphql_relay_identifier', require: 'graphql/relay/identifier/global_object_identifier'
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ ```bash
19
+ bundle install
20
+ ```
21
+
22
+ If bundler is not being used to manage dependencies, install the gem by executing:
23
+
24
+ ```bash
25
+ gem install graphql_relay_identifier
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ 1. Update your schema to overide the `id_from_object` and `object_from_id` methods and use the
31
+ `GraphQL::Relay::Identifier::GlobalObjectIdentifier` module to handle global identifiers.
32
+
33
+ ```ruby
34
+ class ApplicationSchema < GraphQL::Schema
35
+ # ... other configuration
36
+
37
+ def self.id_from_object(object, type, _query_ctx)
38
+ GraphQL::Relay::Identifier::GlobalObjectIdentifier.to_relay_id(object, type)
39
+ end
40
+
41
+ def self.object_from_id(node_id, _query_ctx)
42
+ GraphQL::Relay::Identifier::GlobalObjectIdentifier.from_relay_id(node_id)
43
+ end
44
+ end
45
+ ```
46
+
47
+ 2. _RECOMMENDED_: Set the `GraphQL::Schema::UniqueWithinType.default_id_separator` to a `:` (colon).
48
+ This is used to deliminate the GraphQL type name from the identifier fields in the global object
49
+ identifier.
50
+
51
+ ```ruby
52
+ # config/initializers/graphql.rb
53
+ GraphQL::Schema::UniqueWithinType.default_id_separator = ':'
54
+ ```
55
+
56
+ 3. _ADVANCED CONFIGURATION_: Introduce an initializer to set up any custom identifier configurations
57
+ for your models and types, as needed.
58
+
59
+ _NOTE: This is only necessary if you have models that do not use the default `id` field as their
60
+ primary key._
61
+
62
+ ```ruby
63
+ # config/initializers/graphql_relay_identifier.rb
64
+ GraphQL::Relay::Identifier::GlobalObjectIdentifier.add_identifier 'BlogPost', :slug
65
+ GraphQL::Relay::Identifier::GlobalObjectIdentifier.add_identifier 'ModelWithComplexKey', :field1, :field2,
66
+ GraphQL::Relay::Identifier::GlobalObjectIdentifier.add_identifier 'ModelWithVirtualAttribute', :id, virtual_attribute_names: %i[some_virtual_attribute]
67
+ ```
68
+
69
+ ### `GraphQL::Relay::Identifier::GlobalObjectIdentifier.add_identifier`
70
+
71
+ This method allows you to define a global identifier for a specific model when the default (`id`) is
72
+ not suitable.
73
+
74
+ You can specify one or more fields to be used as the identifier, and optionally, you can define
75
+ virtual attributes that should be included in the identifier. Virtual attributes are not persisted
76
+ in the database but play a role in how a model instance behaves and is identified.
77
+
78
+ ### Format of a GraphQL Relay Identifier
79
+
80
+ Suppose you have a model `BlogPost` with a `slug` field that is used as the primary key for the
81
+ model. Assuming the GraphQL Type for this model is `BlogPost`, the identifier for a specific
82
+ instance of this model with a `slug` value of `my-awesome-blog-post` would be represented as:
83
+
84
+ ```
85
+ Base64.strict_encode64('BlogPost:{"v":1,"klass":"BlogPost","slug":"my-awesome-blog-post"}')
86
+ ```
87
+
88
+ This identifier can be used in GraphQL queries to uniquely identify the `BlogPost` instance with:
89
+
90
+ - An encoding version of 1 (the default version)
91
+ - A model name of `BlogPost`
92
+ - A single field `slug` with the unique value `my-awesome-blog-post`
93
+
94
+ ## Development
95
+
96
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run
97
+ the tests. You can also run `bin/console` for an interactive prompt that will allow you to
98
+ experiment.
99
+
100
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new
101
+ version, update the version number in `version.rb`, and then run `bundle exec rake release`, which
102
+ will create a git tag for the version, push git commits and the created tag, and push the `.gem`
103
+ file to [rubygems.org](https://rubygems.org).
104
+
105
+ ## Contributing
106
+
107
+ Bug reports and pull requests are welcome on GitHub at
108
+ https://github.com/mjfaga/graphql_relay_identifier. This project is intended to be a safe, welcoming
109
+ space for collaboration, and contributors are expected to adhere to the
110
+ [code of conduct](https://github.com/mjfaga/graphql_relay_identifier/blob/main/CODE_OF_CONDUCT.md).
111
+
112
+ ## License
113
+
114
+ The gem is available as open source under the terms of the [MIT License](./LICENSE.txt).
115
+
116
+ ## Code of Conduct
117
+
118
+ Everyone interacting in the GraphQL::Relay::Identifier project's codebases, issue trackers, chat
119
+ rooms and mailing lists is expected to follow the
120
+ [code of conduct](https://github.com/mjfaga/graphql_relay_identifier/blob/main/CODE_OF_CONDUCT.md).
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "graphql"
4
+
5
+ module GraphQL
6
+ module Relay
7
+ module Identifier
8
+ # GlobalObjectIdentifier is a utility class that provides a way to create and manage unique identifiers for
9
+ # entities implementing a Node in a GraphQL schema.
10
+ #
11
+ # This implementation is ORM-agnostic but assumes certain methods like find_by! are available
12
+ # on your model classes. You may need to customize the find method based on your actual ORM.
13
+ class GlobalObjectIdentifier
14
+ class << self
15
+ private
16
+
17
+ def identifiers
18
+ @identifiers ||= Hash.new { |h, klass| h[klass] = Identifier.build_for(klass) }
19
+ end
20
+
21
+ attr_writer :identifiers
22
+ end
23
+
24
+ # Identifier is a class that represents a unique identifier for a specific class in the GraphQL schema.
25
+ class Identifier
26
+ class << self
27
+ attr_writer :attribute_names, :virtual_attribute_names, :version
28
+
29
+ attr_accessor :klass
30
+
31
+ def attribute_names
32
+ @attribute_names ||= %i[id]
33
+ end
34
+
35
+ def version
36
+ @version ||= 1
37
+ end
38
+
39
+ def virtual_attribute_names
40
+ @virtual_attribute_names ||= %i[]
41
+ end
42
+ end
43
+
44
+ def self.as_json(object)
45
+ attributes = attribute_names.each_with_object({}) do |key, hash|
46
+ hash[key] = object.public_send(key)
47
+ hash
48
+ end
49
+ virtual_attributes = virtual_attribute_names.each_with_object({}) do |key, hash|
50
+ hash[key] = object.public_send(key)
51
+ hash
52
+ end
53
+
54
+ {
55
+ v: version,
56
+ klass:,
57
+ **attributes,
58
+ **virtual_attributes
59
+ }
60
+ end
61
+
62
+ def self.build_for(klass, *args)
63
+ const_set(klass.gsub("::", ""), Class.new(self)).tap do |identifier_class|
64
+ attribute_names = []
65
+ virtual_attribute_names = []
66
+ args.each do |attribute_name|
67
+ if attribute_name.is_a?(Symbol)
68
+ attribute_names << attribute_name
69
+ elsif attribute_name.is_a?(Hash) && attribute_name.key?(:virtual_attribute_names)
70
+ virtual_attr_names = attribute_name[:virtual_attribute_names]
71
+ virtual_attr_names = [virtual_attr_names] unless virtual_attr_names.is_a?(Array)
72
+ virtual_attr_names.each do |inner_attribute_name|
73
+ virtual_attribute_names << inner_attribute_name
74
+ end
75
+ end
76
+ end
77
+
78
+ identifier_class.klass = klass
79
+ identifier_class.attribute_names = attribute_names unless attribute_names.empty?
80
+ identifier_class.virtual_attribute_names = virtual_attribute_names unless virtual_attribute_names.empty?
81
+ identifier_class.define_attribute_methods
82
+ end
83
+ end
84
+
85
+ def self.define_attribute_methods
86
+ attribute_names.each do |attribute_name|
87
+ define_method(attribute_name) { @hash[attribute_name.to_s] }
88
+ end
89
+
90
+ virtual_attribute_names.each do |attribute_name|
91
+ define_method(attribute_name) { @hash[attribute_name.to_s] }
92
+ end
93
+ end
94
+
95
+ def self.remove_for(klass)
96
+ remove_const(klass.gsub("::", ""))
97
+ end
98
+
99
+ def initialize(hash)
100
+ @hash = hash
101
+ end
102
+
103
+ def attributes
104
+ self.class.attribute_names.each_with_object({}) do |key, hash|
105
+ hash[key] = public_send(key)
106
+ hash
107
+ end
108
+ end
109
+
110
+ def find
111
+ # Convert string class name to actual constant
112
+ klass_constant = self.class.klass.split("::").inject(Object) do |mod, class_name|
113
+ mod.const_get(class_name)
114
+ end
115
+
116
+ # This implementation assumes the class has a find_by! method that accepts
117
+ # attributes as keyword arguments. If using a different ORM or data access
118
+ # pattern, you'll need to customize this method.
119
+ instance = klass_constant.find_by!(**attributes)
120
+
121
+ # Assign virtual attributes if any exist
122
+ unless virtual_attributes.empty?
123
+ virtual_attributes.each do |key, value|
124
+ instance.public_send("#{key}=", value) if instance.respond_to?("#{key}=")
125
+ end
126
+ end
127
+
128
+ instance
129
+ end
130
+
131
+ def virtual_attributes
132
+ self.class.virtual_attribute_names.each_with_object({}) do |key, hash|
133
+ hash[key] = public_send(key)
134
+ hash
135
+ end
136
+ end
137
+ end
138
+
139
+ class << self
140
+ def add_identifier(klass, *attribute_names)
141
+ identifiers[klass] = Identifier.build_for(klass, *attribute_names)
142
+ end
143
+
144
+ def from_relay_id(node_id)
145
+ parse(node_id)&.find
146
+ end
147
+
148
+ def parse(node_id)
149
+ _typename, json = GraphQL::Schema::UniqueWithinType.decode(node_id)
150
+
151
+ return unless json
152
+
153
+ hash = JSON.parse(json)
154
+ klass = hash["klass"]
155
+
156
+ identifiers[klass].new(hash)
157
+ rescue JSON::ParserError, GraphQL::ExecutionError
158
+ nil
159
+ end
160
+
161
+ def remove_identifier(klass)
162
+ identifiers.delete(klass)
163
+ Identifier.remove_for(klass)
164
+ end
165
+
166
+ def to_relay_id(object, type)
167
+ unsafe_to_relay_id(object, graphql_name: type.graphql_name, object_class_name: object.class.name)
168
+ end
169
+
170
+ def unsafe_to_relay_id(object, graphql_name:, object_class_name:)
171
+ # This allows us to get relay ids for types without having to access the GraphQL type, but without the safety
172
+ # or consistency of the GraphQL type. Used by external callers (e.g. the API) to generate relay ids.
173
+ GraphQL::Schema::UniqueWithinType.encode(
174
+ graphql_name,
175
+ identifiers[object_class_name].as_json(object).to_json
176
+ )
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Relay
5
+ module Identifier
6
+ VERSION = "0.1.0"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Relay
5
+ # Library module for GraphQL Relay Identifier
6
+ module Identifier
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "graphql/relay/identifier/global_object_identifier"
@@ -0,0 +1,42 @@
1
+ module GraphQL
2
+ module Relay
3
+ module Identifier
4
+ class GlobalObjectIdentifier
5
+ # Class methods
6
+ def self.add_identifier: (String klass, *Symbol | Hash[Symbol, Array[Symbol] | Symbol] attribute_names) -> Identifier
7
+ def self.from_relay_id: (String node_id) -> untyped
8
+ def self.parse: (String node_id) -> Identifier?
9
+ def self.remove_identifier: (String klass) -> void
10
+ def self.to_relay_id: (untyped object, untyped type) -> String
11
+ def self.unsafe_to_relay_id: (untyped object, graphql_name: String, object_class_name: String) -> String
12
+
13
+ private
14
+ def self.identifiers: () -> Hash[String, Identifier]
15
+ def self.identifiers=: (Hash[String, Identifier]) -> void
16
+
17
+ # Identifier class
18
+ class Identifier
19
+ # Class methods
20
+ def self.attribute_names: () -> Array[Symbol]
21
+ def self.attribute_names=: (Array[Symbol]) -> void
22
+ def self.version: () -> Integer
23
+ def self.version=: (Integer) -> void
24
+ def self.virtual_attribute_names: () -> Array[Symbol]
25
+ def self.virtual_attribute_names=: (Array[Symbol]) -> void
26
+ def self.klass: () -> String
27
+ def self.klass=: (String) -> void
28
+ def self.as_json: (untyped object) -> Hash[Symbol | String, untyped]
29
+ def self.build_for: (String klass, *Symbol | Hash[Symbol, Array[Symbol] | Symbol]) -> Identifier
30
+ def self.define_attribute_methods: () -> void
31
+ def self.remove_for: (String klass) -> void
32
+
33
+ # Instance methods
34
+ def initialize: (Hash[String, untyped] hash) -> void
35
+ def attributes: () -> Hash[Symbol, untyped]
36
+ def find: () -> untyped
37
+ def virtual_attributes: () -> Hash[Symbol, untyped]
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,7 @@
1
+ module GraphQL
2
+ module Relay
3
+ module Identifier
4
+ VERSION: String
5
+ end
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,128 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: graphql_relay_identifier
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mark Faga
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: graphql
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rubocop
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.21'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.21'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rubocop-rspec
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ description: This gem provides an implementation the Relay Global Object Identification
83
+ specification in Ruby, ensuring compatibility with GraphQL Federation. It allows
84
+ you to define and resolve global identifiers for your GraphQL objects, making it
85
+ easier to work with Relay and Federation in your applications.
86
+ email:
87
+ - mjfaga@gmail.com
88
+ executables: []
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - CHANGELOG.md
93
+ - LICENSE.txt
94
+ - README.md
95
+ - lib/graphql/relay/identifier.rb
96
+ - lib/graphql/relay/identifier/global_object_identifier.rb
97
+ - lib/graphql/relay/identifier/version.rb
98
+ - lib/graphql_relay_identifier.rb
99
+ - sig/graphql/relay/identifier.rbs
100
+ - sig/graphql/relay/identifier/global_object_identifier.rbs
101
+ homepage: https://github.com/mjfaga/graphql_relay_identifier
102
+ licenses:
103
+ - MIT
104
+ metadata:
105
+ allowed_push_host: https://rubygems.org
106
+ homepage_uri: https://github.com/mjfaga/graphql_relay_identifier
107
+ source_code_uri: https://github.com/mjfaga/graphql_relay_identifier
108
+ changelog_uri: https://github.com/mjfaga/graphql_relay_identifier/blob/main/CHANGELOG.md
109
+ bug_tracker_uri: https://github.com/mjfaga/graphql_relay_identifier/issues
110
+ rdoc_options: []
111
+ require_paths:
112
+ - lib
113
+ required_ruby_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '3.1'
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ requirements: []
124
+ rubygems_version: 3.6.7
125
+ specification_version: 4
126
+ summary: A GraphQL Relay Global Object Identification implementation for Ruby that
127
+ is Federation compatible.
128
+ test_files: []