graphiti_graphql 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f3636c8634e8e8798b5faa86fed69735c02be79903ecca7f4fb09418786756f1
4
- data.tar.gz: 6ce1f38785202d1fb6972152731ef5f535fd4f3514f8996eb60f1abc13431ea0
3
+ metadata.gz: 94c56621cb36c796bbefd0e8f626d90e359e2e8740b2cb443a8293e2f98b57df
4
+ data.tar.gz: dd1d065a140a292adab0f5d3c54e426dfcfa55b3691523f50ceb947fa752fdbd
5
5
  SHA512:
6
- metadata.gz: 45796a4cde424e1fcec4d8eef45238fb52d4fb4d0980c067dbd8321a9dd1b9e3f5d487a3125d9fe0b50bb0d2a452bf79520e5cee4438ee7f31da229c51b70e98
7
- data.tar.gz: c1f7707ea8591be326b7a371b797bd15a969cdf5c115e35892966c310dcb75be656d4a81b408b6c72d83a192a3d0dc69042b91a6006d3220b9fa31a8134eb004
6
+ metadata.gz: 94886a1ebbed6fee376239b1a10c197c3891cd280aad29973a7a1ce4d6f769e9c1fc3949bb4613dbe98c44d1f584a1b2863955c605798525e24ee1766b136891
7
+ data.tar.gz: d0ca0298e0e77da84361a1748a2b36457569910d43aed602a94cfd6eb8869c32c8a0db611bca68dbf64382fe0c5cd8dddb8956c105af9ae7819c4e92cce03adc
data/Gemfile CHANGED
@@ -3,4 +3,4 @@ source "https://rubygems.org"
3
3
  # Specify your gem's dependencies in graphiti_graphql.gemspec
4
4
  gemspec
5
5
 
6
- gem "graphiti", path: "../graphiti"
6
+ gem "graphiti"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ../graphiti
3
3
  specs:
4
- graphiti (1.2.31)
4
+ graphiti (1.2.32)
5
5
  activesupport (>= 4.1)
6
6
  concurrent-ruby (~> 1.0)
7
7
  dry-types (>= 0.15.0, < 2.0)
@@ -12,7 +12,7 @@ PATH
12
12
  PATH
13
13
  remote: .
14
14
  specs:
15
- graphiti_graphql (0.1.0)
15
+ graphiti_graphql (0.1.1)
16
16
  activesupport (>= 4.1)
17
17
  graphql (~> 1.12)
18
18
 
@@ -35,7 +35,7 @@ GEM
35
35
  coderay (1.1.3)
36
36
  concurrent-ruby (1.1.8)
37
37
  diff-lcs (1.4.4)
38
- dry-configurable (0.12.0)
38
+ dry-configurable (0.12.1)
39
39
  concurrent-ruby (~> 1.0)
40
40
  dry-core (~> 0.5, >= 0.5.0)
41
41
  dry-container (0.7.2)
@@ -47,7 +47,7 @@ GEM
47
47
  dry-logic (1.1.0)
48
48
  concurrent-ruby (~> 1.0)
49
49
  dry-core (~> 0.5, >= 0.5)
50
- dry-types (1.5.0)
50
+ dry-types (1.5.1)
51
51
  concurrent-ruby (~> 1.0)
52
52
  dry-container (~> 0.3)
53
53
  dry-core (~> 0.5, >= 0.5)
data/README.md CHANGED
@@ -1,35 +1,159 @@
1
1
  # GraphitiGraphql
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/graphiti_graphql`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ GraphQL (and Apollo Federation) support for Graphiti. Serve traditional Rails JSON, JSON:API or GraphQL with the same codebase.
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ Currently read-only, but you can add your own Mutations [manually](#blending-with-graphql-ruby).
6
6
 
7
- ## Installation
7
+ ## Setup
8
8
 
9
- Add this line to your application's Gemfile:
9
+ Add to your `Gemfile`:
10
+
11
+ ```rb
12
+ gem 'graphiti', ">= 1.2.32"
13
+ gem "graphiti_graphql"
14
+ ```
15
+
16
+ Mount the engine:
10
17
 
11
18
  ```ruby
12
- gem 'graphiti_graphql'
19
+ # config/routes.rb
20
+ Rails.application.routes.draw do
21
+ scope path: ApplicationResource.endpoint_namespace, defaults: { format: :jsonapi } do
22
+ # ... normal graphiti stuff ...
23
+
24
+ mount GraphitiGraphQL::Engine, at: "/gql"
25
+ end
26
+ end
13
27
  ```
14
28
 
15
- And then execute:
29
+ For a default Graphiti app, you can now serve GraphQL by POSTing to `/api/v1/gql`.
30
+
31
+ That's it 🎉!
16
32
 
17
- $ bundle
33
+ #### GraphiQL
34
+
35
+ You can add the GraphiQL editor to the project via [graphiql-rails](https://github.com/rmosolgo/graphiql-rails) as normal, but to save you the time here are the steps to make it work when Rails is running in API-only mode:
36
+
37
+ Add to the Gemfile:
38
+
39
+ ```ruby
40
+ gem "graphiql-rails"
41
+ gem 'sprockets', '~> 3' # https://github.com/rmosolgo/graphiql-rails/issues/53
42
+ ```
18
43
 
19
- Or install it yourself as:
44
+ And then in `config/application.rb`:
20
45
 
21
- $ gem install graphiti_graphql
46
+ ```ruby
47
+ # *Uncomment* this line!
48
+ # require "sprockets/railtie"
49
+ ```
22
50
 
23
51
  ## Usage
24
52
 
25
- TODO: Write usage instructions here
53
+ #### Blending with graphql-ruby
54
+
55
+ Define your Schema and Type classes as normal. Then in an initializer:
26
56
 
27
- ## Development
57
+ ```ruby
58
+ # config/initializers/graphiti.rb
59
+ GraphitiGraphQL.schema_class = MySchema
60
+ ```
28
61
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
62
+ Any pre-existing GraphQL endpoint will continue working as normal. But the GQL endpoint you mounted in `config/routes.rb` will now serve BOTH your low-level `graphql-ruby` schema AND your Graphiti-specific schema. Note these cannot (currently) be served side-by-side on under `query` within the *same request*.
30
63
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
64
+ By default the GraphQL context will be `Graphiti.context[:object]`, which is the controller being called. You might want to customize this so your existing graphql-ruby code continues to expect the same context:
32
65
 
33
- ## Contributing
66
+ ```ruby
67
+ GraphitiGraphQL.define_context do |controller|
68
+ { current_user: controller.current_user }
69
+ end
70
+ ```
34
71
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/graphiti_graphql.
72
+ #### Adding Federation Support
73
+
74
+ Add to the Gemfile
75
+
76
+ ```ruby
77
+ gem "apollo-federation"
78
+ gem "graphql-batch"
79
+ ```
80
+
81
+ And change the way we require `graphiti_graphql`:
82
+
83
+ ```ruby
84
+ gem "graphiti_graphql", require: "graphiti_graphql/federation"
85
+ ```
86
+
87
+ To create a federated relationship:
88
+
89
+ ```ruby
90
+ # PositionResource
91
+ federated_belongs_to :employee
92
+ ```
93
+
94
+ Or pass `type` and/or `foreign_key` to customize:
95
+
96
+ ```ruby
97
+ # type here is the GraphQL Type
98
+ federated_belongs_to :employee, type: "MyEmployee", foreign_key: :emp_id
99
+ ```
100
+
101
+ For `has_many` it's a slightly different syntax because we're adding the relationship to the ***remote*** type:
102
+
103
+ ```ruby
104
+ federated_type("Employee").has_many :positions # foreign_key: optional
105
+ ```
106
+
107
+ Finally, `has_many` accepts the traditional `params` block that works as normal:
108
+
109
+ ```ruby
110
+ federated_type("Employee").has_many :positions do
111
+ params do |hash|
112
+ hash[:filter][:active] = true
113
+ hash[:sort] = "-title"
114
+ end
115
+ end
116
+ ```
117
+
118
+ Remember that any time you make a change that affects the schema, you will have to bounce your federation gateway. This is how Apollo Federation works when not in "managed" mode and is unrelated to `graphiti_graphql`.
119
+
120
+ ## Configuration
121
+
122
+ #### Entrypoints
123
+
124
+ By default all Graphiti resources will expose their `index` and `show` functionality. IOW `EmployeeResource` now serves a list at `Query#employees` and a single employee at `Query#employee(id: 123)`. To limit the entrypoints:
125
+
126
+ ```ruby
127
+ GraphitiGraphQL::Schema.entrypoints = [
128
+ EmployeeResource
129
+ ]
130
+ ```
131
+
132
+ #### Schema Reloading
133
+
134
+ You may want to automatically regenerate the GQL schema when when Rails reloads your classes, or you may not want to pay that performance penalty. To turn off the automatic reloading:
135
+
136
+ ```ruby
137
+ # config/initializers/graphiti.rb
138
+ GraphitiGraphQL.config.schema_reloading = false
139
+ ```
140
+
141
+ #### `.graphql_entrypoint`
142
+
143
+ If the field you want on `Query` can't be inferred from the class name:
144
+
145
+ ```ruby
146
+ class EmployeeResource < ApplicationResource
147
+ self.graphql_entrypoint = :workers
148
+ end
149
+ ```
150
+
151
+ You can now
152
+
153
+ ```
154
+ query {
155
+ workers {
156
+ firstName
157
+ }
158
+ }
159
+ ```
@@ -27,6 +27,7 @@ Gem::Specification.new do |spec|
27
27
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
28
  spec.require_paths = ["lib"]
29
29
 
30
+ spec.add_dependency "graphiti", ">= 1.2.33"
30
31
  spec.add_dependency "activesupport", ">= 4.1"
31
32
  spec.add_dependency "graphql", "~> 1.12"
32
33
 
@@ -11,38 +11,58 @@ require "graphiti_graphql/schema"
11
11
  require "graphiti_graphql/runner"
12
12
  require "graphiti_graphql/util"
13
13
 
14
- Graphiti.class_eval do
15
- class << self
16
- attr_writer :graphql_schema
17
- end
14
+ module GraphitiGraphQL
15
+ class Configuration
16
+ attr_accessor :schema_reloading
18
17
 
19
- # TODO probably move these off of Graphiti
20
- def self.gql(query, variables)
21
- runner = ::GraphitiGraphQL::Runner.new
22
- runner.execute(query, variables, graphql_schema.schema)
23
- end
18
+ def initialize
19
+ self.schema_reloading = true
20
+ end
24
21
 
25
- def self.graphql_schema
26
- @graphql_schema ||= GraphitiGraphQL::Schema.generate
27
- end
22
+ def define_context(&blk)
23
+ @define_context = blk
24
+ end
28
25
 
29
- def self.graphql_schema?
30
- !!@graphql_schema
26
+ def get_context
27
+ obj = Graphiti.context[:object]
28
+ if @define_context
29
+ @define_context.call(obj)
30
+ else
31
+ {object: obj}
32
+ end
33
+ end
31
34
  end
32
35
 
33
- def self.graphql_schema!(entrypoint_resources = nil)
34
- @graphql_schema = GraphitiGraphQL::Schema.generate(entrypoint_resources)
36
+ module Runnable
37
+ def gql(query, variables)
38
+ runner = ::GraphitiGraphQL::Runner.new
39
+ runner.execute(query, variables, GraphitiGraphQL.schemas.graphql)
40
+ end
35
41
  end
36
- end
37
42
 
38
- module GraphitiGraphQL
39
- class Error < StandardError; end
43
+ class SchemaProxy
44
+ def graphql
45
+ generated.schema
46
+ end
40
47
 
41
- class Configuration
42
- attr_accessor :schema_reloading
48
+ def graphiti
49
+ generated.graphiti_schema
50
+ end
43
51
 
44
- def initialize
45
- self.schema_reloading = true
52
+ def generated
53
+ @generated ||= GraphitiGraphQL::Schema.generate
54
+ end
55
+
56
+ def generate!(entrypoint_resources = nil)
57
+ @generated = GraphitiGraphQL::Schema.generate(entrypoint_resources)
58
+ end
59
+
60
+ def generated?
61
+ !!@generated
62
+ end
63
+
64
+ def clear!
65
+ @generated = nil
46
66
  end
47
67
  end
48
68
 
@@ -54,20 +74,13 @@ module GraphitiGraphQL
54
74
  @config ||= Configuration.new
55
75
  end
56
76
 
57
- def self.define_context(&blk)
58
- @define_context = blk
59
- end
60
-
61
- def self.get_context
62
- obj = Graphiti.context[:object]
63
- if @define_context
64
- @define_context.call(obj)
65
- else
66
- {object: obj}
67
- end
77
+ def self.schemas
78
+ @schemas ||= SchemaProxy.new
68
79
  end
69
80
  end
70
81
 
82
+ Graphiti.extend(GraphitiGraphQL::Runnable)
83
+
71
84
  if defined?(::Rails)
72
85
  require "graphiti_graphql/engine"
73
86
  end
@@ -48,16 +48,6 @@ module GraphitiGraphQL
48
48
  end
49
49
  end
50
50
 
51
- # We store a list of all federated relationships
52
- # Clear that list when classes are cleared so we don't keep appending
53
- initializer "graphiti_graphql.clear_federation" do |app|
54
- if defined?(GraphitiGraphQL::Federation)
55
- GraphitiGraphQL::Engine.reloader_class.after_class_unload do
56
- GraphitiGraphQL::Federation.clear!
57
- end
58
- end
59
- end
60
-
61
51
  initializer "graphiti_graphql.schema_reloading" do |app|
62
52
  # Only reload the schema if we ask for it
63
53
  # Some may want to avoid the performance penalty
@@ -66,8 +56,8 @@ module GraphitiGraphQL
66
56
  # We want to reload the schema when classes change
67
57
  # But this way, you only pay the cost (time) when the GraphQL endpoint
68
58
  # is actually hit
69
- if Graphiti.graphql_schema?
70
- Graphiti.graphql_schema = nil
59
+ if GraphitiGraphQL.schemas.generated?
60
+ GraphitiGraphQL.schemas.clear!
71
61
  end
72
62
  end
73
63
  end
@@ -12,27 +12,28 @@ end
12
12
 
13
13
  # We don't want to add these as dependencies,
14
14
  # but do need to check things don't break
15
- if Gem::Version.new(ApolloFederation::VERSION) >= Gem::Version.new('2.0.0')
15
+ if Gem::Version.new(ApolloFederation::VERSION) >= Gem::Version.new("2.0.0")
16
16
  raise "graphiti_graphql federation is incompatible with apollo-federation >= 2"
17
17
  end
18
18
 
19
- if Gem::Version.new(GraphQL::Batch::VERSION) >= Gem::Version.new('1.0.0')
19
+ if Gem::Version.new(GraphQL::Batch::VERSION) >= Gem::Version.new("1.0.0")
20
20
  raise "graphiti_graphql federation is incompatible with graphql-batch >= 1"
21
21
  end
22
22
 
23
23
  require "graphiti_graphql"
24
+ require "graphiti_graphql/federation/loaders/has_many"
25
+ require "graphiti_graphql/federation/loaders/belongs_to"
26
+ require "graphiti_graphql/federation/federated_resource"
27
+ require "graphiti_graphql/federation/federated_relationship"
28
+ require "graphiti_graphql/federation/resource_dsl"
29
+ require "graphiti_graphql/federation/apollo_federation_override"
30
+ require "graphiti_graphql/federation/schema_decorator"
24
31
 
25
32
  module GraphitiGraphQL
26
33
  module Federation
27
-
28
- def self.external_resources
29
- @external_resources ||= {}
30
- end
31
-
32
- def self.clear!
33
- @external_resources = {}
34
- end
35
-
34
+ # * Extend Graphiti::Resource with federated_* macros
35
+ # * Add apollo-federation modules to graphql-ruby base types
36
+ # * Mark federation = true for checks down the line
36
37
  def self.setup!
37
38
  Graphiti::Resource.send(:include, ResourceDSL)
38
39
  schema = GraphitiGraphQL::Schema
@@ -50,214 +51,5 @@ module GraphitiGraphQL
50
51
  schema.base_interface.field_class(schema.base_field)
51
52
  GraphitiGraphQL::Schema.federation = true
52
53
  end
53
-
54
- class HasManyLoader < GraphQL::Batch::Loader
55
- def initialize(resource_class, params, foreign_key)
56
- @resource_class = resource_class
57
- @params = params
58
- @foreign_key = foreign_key
59
- end
60
-
61
- def perform(ids)
62
- @params[:filter] ||= {}
63
- @params[:filter].merge!(@foreign_key => { eq: ids.join(",") })
64
-
65
- if ids.length > 1 && @params[:page]
66
- raise Graphiti::Errors::UnsupportedPagination
67
- elsif !@params[:page]
68
- @params[:page] = { size: 999 }
69
- end
70
-
71
- Util.with_gql_context do
72
- records = @resource_class.all(@params).as_json[:data]
73
- fk = ->(record) { record[@foreign_key].to_s }
74
- map = records.group_by(&fk)
75
- ids.each do |id|
76
- fulfill(id, (map[id] || []))
77
- end
78
- end
79
- end
80
- end
81
-
82
- class BelongsToLoader < GraphQL::Batch::Loader
83
- def initialize(resource_class, fields)
84
- @resource_class = resource_class
85
- @fields = fields
86
- end
87
-
88
- def perform(ids)
89
- Util.with_gql_context do
90
- params = { filter: { id: { eq: ids.join(",") } } }
91
- params[:fields] = { @resource_class.type => @fields.join(",") }
92
- records = @resource_class.all(params).as_json[:data]
93
- pk = ->(record) { record[:id].to_s }
94
- map = records.index_by(&pk)
95
- ids.each { |id| fulfill(id, map[id]) }
96
- end
97
- end
98
- end
99
-
100
- class ExternalRelationship
101
- attr_reader :name, :local_resource_class, :foreign_key
102
-
103
- def initialize(kind, name, local_resource_class, foreign_key)
104
- @kind = kind
105
- @name = name
106
- @local_resource_class = local_resource_class
107
- @foreign_key = foreign_key
108
- end
109
-
110
- def has_many?
111
- @kind == :has_many
112
- end
113
-
114
- def belongs_to?
115
- @kind == :belongs_to
116
- end
117
- end
118
-
119
- class ExternalResource
120
- attr_reader :type_name, :relationships
121
-
122
- def initialize(type_name)
123
- @type_name = type_name
124
- @relationships = {}
125
- end
126
-
127
- def add_relationship(
128
- kind,
129
- name,
130
- local_resource_class,
131
- foreign_key
132
- )
133
- @relationships[name] = ExternalRelationship
134
- .new(kind, name, local_resource_class, foreign_key)
135
- end
136
- end
137
-
138
- class TypeProxy
139
- def initialize(caller, type_name)
140
- @caller = caller
141
- @type_name = type_name
142
- end
143
-
144
- def has_many(relationship_name, foreign_key: nil)
145
- @caller.federated_has_many relationship_name,
146
- type: @type_name,
147
- foreign_key: foreign_key
148
- end
149
- end
150
-
151
- module ResourceDSL
152
- extend ActiveSupport::Concern
153
-
154
- class_methods do
155
- def federated_type(type_name)
156
- TypeProxy.new(self, type_name)
157
- end
158
-
159
- # TODO: raise error if belongs_to doesn't have corresponding filter (on schema gen)
160
- # TODO: hang these on the resource classes themselves
161
- def federated_has_many(name, type:, foreign_key: nil)
162
- foreign_key ||= :"#{type.underscore}_id"
163
- resource = GraphitiGraphQL::Federation.external_resources[type] ||=
164
- ExternalResource.new(type)
165
- resource.add_relationship(:has_many, name, self, foreign_key)
166
-
167
- attribute = attributes.find do |name, config|
168
- name.to_sym == foreign_key && !!config[:readable] && !!config[:filterable]
169
- end
170
- has_filter = filters.key?(foreign_key)
171
- if !attribute && !has_filter
172
- attribute foreign_key, :integer,
173
- only: [:readable, :filterable],
174
- schema: false,
175
- readable: :gql?,
176
- filterable: :gql?
177
- elsif has_filter && !attribute
178
- prior = filters[foreign_key]
179
- attribute foreign_key, prior[:type],
180
- only: [:readable, :filterable],
181
- schema: false,
182
- readable: :gql?
183
- filters[foreign_key] = prior
184
- elsif attribute && !has_filter
185
- filter foreign_key, attribute[:type]
186
- end
187
- end
188
-
189
- def federated_belongs_to(name, type: nil, foreign_key: nil)
190
- type ||= name.to_s.camelize
191
- foreign_key ||= :"#{name.to_s.underscore}_id"
192
- resource = GraphitiGraphQL::Federation.external_resources[type] ||=
193
- ExternalResource.new(type)
194
- resource.add_relationship(:belongs_to, name, self, foreign_key)
195
-
196
- attribute name, :hash, readable: :gql?, only: [:readable], schema: false do
197
- fk = if prc = self.class.attribute_blocks[foreign_key]
198
- instance_eval(&prc)
199
- else
200
- @object.send(foreign_key)
201
- end
202
- {
203
- __typename: type,
204
- id: fk.to_s
205
- }
206
- end
207
- end
208
- end
209
-
210
- def gql?
211
- Graphiti.context[:graphql]
212
- end
213
- end
214
- end
215
- end
216
-
217
- # Hacky sack!
218
- # All we're doing here is adding extras: [:lookahead] to the _entities field
219
- # And passing to to the .resolve_reference method when arity is 3
220
- # This way we can request only fields the user wants when resolving the reference
221
- # Important because we blow up when a field is guarded, and the guard fails
222
- ApolloFederation::EntitiesField::ClassMethods.module_eval do
223
- alias_method :define_entities_field_without_override, :define_entities_field
224
- def define_entities_field(*args)
225
- result = define_entities_field_without_override(*args)
226
- extras = fields["_entities"].extras
227
- extras |= [:lookahead]
228
- fields["_entities"].instance_variable_set(:@extras, extras)
229
- result
230
- end
231
- end
232
-
233
- module EntitiesFieldOverride
234
- def _entities(representations:, lookahead:) # accept the lookahead as argument
235
- representations.map do |reference|
236
- typename = reference[:__typename]
237
- type = context.warden.get_type(typename)
238
- if type.nil? || type.kind != GraphQL::TypeKinds::OBJECT
239
- raise "The _entities resolver tried to load an entity for type \"#{typename}\"," \
240
- ' but no object type of that name was found in the schema'
241
- end
242
-
243
- type_class = type.is_a?(GraphQL::ObjectType) ? type.metadata[:type_class] : type
244
- if type_class.respond_to?(:resolve_reference)
245
- meth = type_class.method(:resolve_reference)
246
- # ** THIS IS OUR EDIT **
247
- result = if meth.arity == 3
248
- type_class.resolve_reference(reference, context, lookahead)
249
- else
250
- type_class.resolve_reference(reference, context)
251
- end
252
- else
253
- result = reference
254
- end
255
-
256
- context.schema.after_lazy(result) do |resolved_value|
257
- context[resolved_value] = type
258
- resolved_value
259
- end
260
- end
261
54
  end
262
55
  end
263
- ApolloFederation::EntitiesField.send :prepend, EntitiesFieldOverride