lighthouse-graphql 0.3.0 → 0.5.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.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +182 -1
  3. data/README.md +18 -7
  4. data/docs/best-practices.md +16 -6
  5. data/docs/configuration.md +33 -8
  6. data/docs/directives.md +318 -10
  7. data/docs/events.md +3 -1
  8. data/docs/federation.md +3 -2
  9. data/docs/generators.md +82 -0
  10. data/docs/getting-started.md +66 -32
  11. data/docs/motivation.md +3 -2
  12. data/docs/relationships.md +42 -0
  13. data/docs/relay.md +137 -0
  14. data/docs/resolvers.md +338 -25
  15. data/docs/security.md +227 -0
  16. data/docs/subscriptions.md +60 -37
  17. data/docs/types.md +219 -0
  18. data/docs/uploads.md +80 -0
  19. data/lib/generators/lighthouse/base_classes.rb +70 -0
  20. data/lib/generators/lighthouse/class_generator.rb +19 -8
  21. data/lib/generators/lighthouse/install/install_generator.rb +96 -20
  22. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +19 -10
  23. data/lib/generators/lighthouse/install/templates/schema.rb.tt +21 -2
  24. data/lib/generators/lighthouse/mutation_generator.rb +5 -4
  25. data/lib/generators/lighthouse/query_generator.rb +5 -4
  26. data/lib/generators/lighthouse/subscription_generator.rb +21 -16
  27. data/lib/lighthouse/async_mutation_job.rb +17 -0
  28. data/lib/lighthouse/complexity.rb +175 -0
  29. data/lib/lighthouse/complexity_analyzer.rb +56 -0
  30. data/lib/lighthouse/config.rb +16 -4
  31. data/lib/lighthouse/controller.rb +27 -9
  32. data/lib/lighthouse/dataloader_sources/preload_loader.rb +16 -0
  33. data/lib/lighthouse/dataloader_sources/relation_counts_loader.rb +46 -0
  34. data/lib/lighthouse/default_resolve_dispatcher.rb +26 -0
  35. data/lib/lighthouse/directive_resolvers/all_resolver.rb +1 -1
  36. data/lib/lighthouse/directive_resolvers/argument_binding.rb +61 -0
  37. data/lib/lighthouse/directive_resolvers/argument_transforms_resolver.rb +220 -0
  38. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +2 -2
  39. data/lib/lighthouse/directive_resolvers/async_resolver.rb +90 -0
  40. data/lib/lighthouse/directive_resolvers/base.rb +63 -14
  41. data/lib/lighthouse/directive_resolvers/builder_applier.rb +89 -19
  42. data/lib/lighthouse/directive_resolvers/can_resolvers.rb +1 -1
  43. data/lib/lighthouse/directive_resolvers/convention_resolver.rb +16 -5
  44. data/lib/lighthouse/directive_resolvers/convert_empty_strings_resolver.rb +1 -1
  45. data/lib/lighthouse/directive_resolvers/count_resolver.rb +1 -1
  46. data/lib/lighthouse/directive_resolvers/field_resolver.rb +35 -7
  47. data/lib/lighthouse/directive_resolvers/first_resolver.rb +1 -1
  48. data/lib/lighthouse/directive_resolvers/global_id_resolver.rb +57 -0
  49. data/lib/lighthouse/directive_resolvers/lazy_load_resolver.rb +61 -0
  50. data/lib/lighthouse/directive_resolvers/limit_resolver.rb +59 -0
  51. data/lib/lighthouse/directive_resolvers/method_resolver.rb +38 -3
  52. data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +1 -1
  53. data/lib/lighthouse/directive_resolvers/namespaced_resolver.rb +31 -0
  54. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +1 -1
  55. data/lib/lighthouse/directive_resolvers/resolution.rb +19 -4
  56. data/lib/lighthouse/directive_resolvers/subscription_resolver.rb +14 -5
  57. data/lib/lighthouse/directive_resolvers/throttle_resolver.rb +77 -0
  58. data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +81 -0
  59. data/lib/lighthouse/directive_resolvers/with_count_resolver.rb +43 -0
  60. data/lib/lighthouse/directive_resolvers/with_resolver.rb +42 -0
  61. data/lib/lighthouse/directives/arguments/more_filters.rb +215 -0
  62. data/lib/lighthouse/directives/definitions.rb +52 -0
  63. data/lib/lighthouse/directives/model_directive.rb +131 -0
  64. data/lib/lighthouse/directives/node_directive.rb +134 -0
  65. data/lib/lighthouse/directives/pagination_manipulator.rb +1 -1
  66. data/lib/lighthouse/directives/registry.rb +21 -20
  67. data/lib/lighthouse/directives/visibility_directive.rb +71 -0
  68. data/lib/lighthouse/enum_directive.rb +41 -0
  69. data/lib/lighthouse/events/execution_trace.rb +2 -1
  70. data/lib/lighthouse/events/lifecycle.rb +3 -1
  71. data/lib/lighthouse/field_resolver_stack.rb +59 -11
  72. data/lib/lighthouse/generated_resolvers.rb +13 -8
  73. data/lib/lighthouse/global_id.rb +31 -0
  74. data/lib/lighthouse/graphql/version.rb +1 -1
  75. data/lib/lighthouse/multipart_request.rb +43 -0
  76. data/lib/lighthouse/node_registry.rb +68 -0
  77. data/lib/lighthouse/pagination.rb +3 -0
  78. data/lib/lighthouse/rb_lighthouse.rb +36 -0
  79. data/lib/lighthouse/relation_counts.rb +29 -0
  80. data/lib/lighthouse/resolve_info.rb +80 -0
  81. data/lib/lighthouse/resolver_bases.rb +68 -0
  82. data/lib/lighthouse/resolver_dispatch.rb +36 -11
  83. data/lib/lighthouse/scalar_coercion.rb +72 -0
  84. data/lib/lighthouse/schema_cache.rb +2 -1
  85. data/lib/lighthouse/schema_factory.rb +0 -1
  86. data/lib/lighthouse/schema_generator.rb +21 -12
  87. data/lib/lighthouse/schema_implementation.rb +4 -8
  88. data/lib/lighthouse/sdl_loader.rb +0 -2
  89. data/lib/lighthouse/support/environment.rb +13 -0
  90. data/lib/lighthouse/support/model_resolver.rb +8 -6
  91. data/lib/lighthouse/support/preload.rb +49 -0
  92. data/lib/lighthouse/type.rb +103 -0
  93. data/lib/lighthouse/type_resolution.rb +150 -0
  94. data/lib/lighthouse/upload.rb +26 -0
  95. data/lib/lighthouse/upload_store.rb +32 -0
  96. data/sig/lighthouse/directives.rbs +3 -2
  97. data/sig/lighthouse/graphql.rbs +29 -0
  98. data/sig/lighthouse.rbs +18 -0
  99. metadata +43 -1
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @model(class: "Post") — map an object type to the ActiveRecord model whose
4
+ # records it resolves, as PHP Lighthouse's directive, for when the type's name
5
+ # is not the model's:
6
+ #
7
+ # type Article @model(class: "Post") { id: ID! title: String! }
8
+ #
9
+ # type Query {
10
+ # articles: [Article!]! @paginate # pages through Post
11
+ # article(id: ID! @eq): Article @find # Post.find_by(id:)
12
+ # }
13
+ #
14
+ # - Every directive that takes `model:` (@all, @find, @first, @paginate, @create,
15
+ # @update, @upsert, @delete, @can*, ...) on a field returning the type gets it,
16
+ # unless it names its own: the SDL is rewritten before the schema is built.
17
+ # - A union or interface resolves a record of the model to the type.
18
+ # - @node finds the type's records in the model.
19
+ # - The schema does not build unless the class is an ActiveRecord model, and a
20
+ # type class's methods (Types::ArticleType) only run on its records.
21
+ class Lighthouse::GraphQL::Directives::ModelDirective
22
+ DEFINITIONS = {
23
+ 'model' => <<~GRAPHQL.chomp
24
+ """
25
+ Map a model class to an object type.
26
+ This can be used when the name of the model differs from the name of the type.
27
+ """
28
+ directive @model(
29
+ "The class name of the corresponding model."
30
+ class: String!
31
+ ) on OBJECT
32
+ GRAPHQL
33
+ }.freeze
34
+
35
+ OBJECTS = [::GraphQL::Language::Nodes::ObjectTypeDefinition, ::GraphQL::Language::Nodes::ObjectTypeExtension].freeze
36
+
37
+ DECLARATION = /directive\s+@model\b/
38
+ USAGE = /@model\b/
39
+
40
+ def self.manipulate(sdl)
41
+ return sdl unless sdl.is_a?(String) && sdl.gsub(DECLARATION, '').match?(USAGE)
42
+
43
+ new(::GraphQL::Language::Parser.parse(sdl)).manipulate(sdl)
44
+ rescue ::GraphQL::ParseError
45
+ sdl
46
+ end
47
+
48
+ # The model class name a schema type maps to with @model, or nil.
49
+ def self.class_name(type)
50
+ directive = (type.ast_node&.directives || []).find { |d| d.name == 'model' }
51
+ directive && Lighthouse::Support::DirectiveArgs.literal(directive, 'class')
52
+ end
53
+
54
+ # The model of a schema type: the one @model names, else the one named after it.
55
+ def self.model_for(type)
56
+ Lighthouse::Support::ModelResolver.constantize(class_name(type) || type.graphql_name)
57
+ end
58
+
59
+ # The model @model declares on a schema type, or nil.
60
+ def self.declared_model(type)
61
+ name = class_name(type)
62
+ name && Lighthouse::Support::ModelResolver.constantize(name)
63
+ end
64
+
65
+ # Fail at build time: every @model names an ActiveRecord model.
66
+ def self.validate!(schema)
67
+ schema.types.each_value do |type|
68
+ name = type.kind.object? && class_name(type)
69
+ next unless name
70
+
71
+ model = declared_model(type)
72
+ next if model.is_a?(Class) && defined?(::ActiveRecord::Base) && model < ::ActiveRecord::Base
73
+
74
+ raise Lighthouse::DefinitionError, "@model(class: #{name.inspect}) on #{type.graphql_name} names no ActiveRecord model."
75
+ end
76
+ end
77
+
78
+ # The directives that take `model:` and act on a field, from the registry.
79
+ def self.model_directives
80
+ Lighthouse::DirectiveRegistry.default.directive_definitions
81
+ .select { |_, definition| definition.match?(/\bmodel:/) && definition.include?('FIELD_DEFINITION') }.keys
82
+ end
83
+
84
+ def initialize(document)
85
+ @document = document
86
+ @models = document.definitions.filter_map { |definition| [definition.name, model_name(definition)] if object?(definition) }
87
+ .select(&:last).to_h
88
+ @directives = self.class.model_directives
89
+ end
90
+
91
+ def manipulate(sdl)
92
+ return sdl if @models.empty?
93
+
94
+ definitions = @document.definitions.map { |definition| object?(definition) ? with_models(definition) : definition }
95
+ ::GraphQL::Language::Printer.new.print(@document.merge(definitions: definitions))
96
+ end
97
+
98
+ private
99
+
100
+ def object?(definition)
101
+ OBJECTS.any? { |kind| definition.is_a?(kind) }
102
+ end
103
+
104
+ # The class @model names on a type definition, or nil.
105
+ def model_name(definition)
106
+ directive = definition.directives.find { |d| d.name == 'model' }
107
+ directive&.arguments&.find { |argument| argument.name == 'class' }&.value
108
+ end
109
+
110
+ def with_models(definition)
111
+ definition.merge(fields: definition.fields.map { |field| with_model(field) })
112
+ end
113
+
114
+ # The field's model directives with `model:` set to the model its return type maps to.
115
+ def with_model(field)
116
+ model = @models[unwrap(field.type).name]
117
+ return field unless model
118
+
119
+ field.merge(directives: field.directives.map { |directive| with_model_argument(directive, model) })
120
+ end
121
+
122
+ def with_model_argument(directive, model)
123
+ return directive unless @directives.include?(directive.name) && directive.arguments.none? { |argument| argument.name == 'model' }
124
+
125
+ directive.merge(arguments: directive.arguments + [::GraphQL::Language::Nodes::Argument.new(name: 'model', value: model)])
126
+ end
127
+
128
+ def unwrap(type)
129
+ type.respond_to?(:of_type) ? unwrap(type.of_type) : type
130
+ end
131
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @node — Relay's global object identification, as PHP Lighthouse's directive.
4
+ # On a type, it makes the schema:
5
+ #
6
+ # "Any object implementing this type can be found by ID through `Query.node`."
7
+ # interface Node @interface(resolveType: "Lighthouse::GraphQL::NodeRegistry") {
8
+ # "Global identifier that can be used to resolve any Node implementation."
9
+ # id: ID!
10
+ # }
11
+ #
12
+ # type User implements Node @node { id: ID! @globalId ... }
13
+ #
14
+ # type Query {
15
+ # node(id: ID! @globalId): Node @field(resolver: "Lighthouse::GraphQL::NodeRegistry@resolve")
16
+ # }
17
+ #
18
+ # leaving alone what the SDL declares itself (a Node interface, a Query.node
19
+ # field, with its own description or middleware). The type keeps its own
20
+ # `id: ID! @globalId`, which encodes the ids `node` decodes. NodeRegistry finds
21
+ # the record: through the type's `model:` or model, or its `resolver:`.
22
+ class Lighthouse::GraphQL::Directives::NodeDirective
23
+ DEFINITIONS = {
24
+ 'node' => <<~GRAPHQL.chomp
25
+ """
26
+ Register a type for Relay's global object identification.
27
+
28
+ When used without any arguments, Lighthouse will attempt
29
+ to resolve the type through a model with the same name.
30
+ """
31
+ directive @node(
32
+ """
33
+ Reference to a class (and method, after `@`) that receives the decoded `id`
34
+ and the context, and returns the object.
35
+
36
+ Mutually exclusive with `model`.
37
+ """
38
+ resolver: String
39
+
40
+ """
41
+ Specify the class name of the model to use.
42
+ This is only needed when the default model detection does not work.
43
+
44
+ Mutually exclusive with `resolver`.
45
+ """
46
+ model: String
47
+ ) on OBJECT
48
+ GRAPHQL
49
+ }.freeze
50
+
51
+ INTERFACE = <<~GRAPHQL.chomp
52
+ "Any object implementing this type can be found by ID through `Query.node`."
53
+ interface Node @interface(resolveType: "Lighthouse::GraphQL::NodeRegistry") {
54
+ "Global identifier that can be used to resolve any Node implementation."
55
+ id: ID!
56
+ }
57
+ GRAPHQL
58
+
59
+ NODE_FIELD = <<~GRAPHQL.chomp
60
+ type Query {
61
+ "Fetches an object given its globally unique `ID`."
62
+ node(id: ID! @globalId): Node @field(resolver: "Lighthouse::GraphQL::NodeRegistry@resolve")
63
+ }
64
+ GRAPHQL
65
+
66
+ OBJECTS = [::GraphQL::Language::Nodes::ObjectTypeDefinition, ::GraphQL::Language::Nodes::ObjectTypeExtension].freeze
67
+
68
+ DECLARATION = /directive\s+@node\b/
69
+ USAGE = /@node\b/
70
+
71
+ def self.manipulate(sdl)
72
+ return sdl unless sdl.is_a?(String) && sdl.gsub(DECLARATION, '').match?(USAGE)
73
+
74
+ new(::GraphQL::Language::Parser.parse(sdl)).manipulate(sdl)
75
+ rescue ::GraphQL::ParseError
76
+ sdl
77
+ end
78
+
79
+ def initialize(document)
80
+ @document = document
81
+ end
82
+
83
+ def manipulate(sdl)
84
+ return sdl unless @document.definitions.any? { |definition| node?(definition) }
85
+
86
+ definitions = @document.definitions.map { |definition| node?(definition) ? implement_node(definition) : definition }
87
+ definitions = with_node_field(definitions)
88
+ printed = ::GraphQL::Language::Printer.new.print(@document.merge(definitions: definitions))
89
+ printed = "#{printed}\n\n#{INTERFACE}" unless declared?('Node')
90
+ # The added @globalId, @field and @interface need their definitions too.
91
+ Lighthouse::GraphQL::SchemaGenerator.inject_directive_definitions(printed)
92
+ end
93
+
94
+ private
95
+
96
+ def node?(definition)
97
+ OBJECTS.any? { |kind| definition.is_a?(kind) } && definition.directives.any? { |directive| directive.name == 'node' }
98
+ end
99
+
100
+ def implement_node(definition)
101
+ return definition if definition.interfaces.any? { |interface| interface.name == 'Node' }
102
+
103
+ definition.merge(interfaces: definition.interfaces + [::GraphQL::Language::Nodes::TypeName.new(name: 'Node')])
104
+ end
105
+
106
+ # Query gains `node`, unless a Query definition or extension has one. With
107
+ # no Query at all, the schema gets one with `node` alone.
108
+ def with_node_field(definitions)
109
+ queries = definitions.select { |definition| query?(definition) }
110
+ return definitions if queries.any? { |query| node_field?(query) }
111
+
112
+ node_query = ::GraphQL::Language::Parser.parse(NODE_FIELD).definitions.first
113
+ return definitions + [node_query] if queries.empty?
114
+
115
+ definitions.map { |definition| definition.equal?(queries.first) ? definition.merge(fields: definition.fields + node_query.fields) : definition }
116
+ end
117
+
118
+ def node_field?(query)
119
+ query.fields.any? { |field| field.name == 'node' }
120
+ end
121
+
122
+ def query?(definition)
123
+ OBJECTS.any? { |kind| definition.is_a?(kind) } && definition.name == query_name
124
+ end
125
+
126
+ def query_name
127
+ schema = @document.definitions.find { |definition| definition.is_a?(::GraphQL::Language::Nodes::SchemaDefinition) }
128
+ schema&.query || 'Query'
129
+ end
130
+
131
+ def declared?(name)
132
+ @document.definitions.any? { |definition| definition.respond_to?(:name) && definition.name == name && !definition.is_a?(::GraphQL::Language::Nodes::ObjectTypeExtension) }
133
+ end
134
+ end
@@ -16,7 +16,7 @@ require_relative 'pagination_info_types'
16
16
  # relation directive (no `type:`) and @all keep returning the list.
17
17
  # The runtime side is Lighthouse::GraphQL::Pagination.
18
18
  class Lighthouse::GraphQL::Directives::PaginationManipulator
19
- RELATION_DIRECTIVES = %w[hasMany belongsToMany morphMany morphToMany].freeze
19
+ RELATION_DIRECTIVES = %w[hasMany hasManyThrough belongsToMany morphMany morphToMany].freeze
20
20
  OBJECT_DEFINITIONS = [
21
21
  ::GraphQL::Language::Nodes::ObjectTypeDefinition,
22
22
  ::GraphQL::Language::Nodes::ObjectTypeExtension
@@ -61,13 +61,17 @@ class Lighthouse::DirectiveRegistry
61
61
  # authorization, argument validation, then @cache (so a cache hit is still
62
62
  # authorized and validated), @clearCache (which acts once the resolver
63
63
  # succeeded), @event and @broadcast (likewise), and finally the data resolver (0).
64
+ # @throttle counts every call, before anything else runs; @async queues what is
65
+ # below @cache, once the request has authorized and validated the call.
64
66
  RESOLUTION_ORDER = {
67
+ 'throttle' => 7,
65
68
  'convertEmptyStringsToNull' => 6,
66
69
  'guard' => 5, 'auth' => 5,
67
70
  'canFind' => 4, 'canModel' => 4, 'canQuery' => 4, 'canResolved' => 4, 'canRoot' => 4,
68
71
  'validator' => 3,
69
- 'cache' => 2,
70
- 'clearCache' => 1, 'event' => 1, 'broadcast' => 1
72
+ 'cache' => 2, 'async' => 2,
73
+ 'clearCache' => 1, 'event' => 1, 'broadcast' => 1,
74
+ 'with' => 1, 'withCount' => 1, 'lazyLoad' => 1, 'globalId' => 1
71
75
  }.freeze
72
76
 
73
77
  def resolution_order(name)
@@ -164,6 +168,8 @@ class Lighthouse::DirectiveRegistry
164
168
  'rename' => :RenameDirectiveResolver,
165
169
  'method' => :MethodDirectiveResolver,
166
170
  'auth' => :AuthDirectiveResolver,
171
+ 'hasManyThrough' => :HasManyDirectiveResolver,
172
+ 'hasOneThrough' => :HasOneDirectiveResolver,
167
173
  'guard' => :GuardDirectiveResolver
168
174
  }.freeze
169
175
 
@@ -181,17 +187,15 @@ class Lighthouse::DirectiveRegistry
181
187
  CacheDirectiveResolver ClearCacheDirectiveResolver EventDirectiveResolver
182
188
  ValidatorDirectiveResolver ConvertEmptyStringsToNullDirectiveResolver
183
189
  SubscriptionDirectiveResolver BroadcastDirectiveResolver
190
+ WithDirectiveResolver WithCountDirectiveResolver LazyLoadDirectiveResolver
191
+ GlobalIdDirectiveResolver NamespacedDirectiveResolver ThrottleDirectiveResolver AsyncDirectiveResolver
184
192
  ].freeze
185
193
 
186
194
  # Argument-builder filter/sort directives.
187
- ARG_BUILDERS = %i[Eq Neq In NotIn Like Where WhereKey OrderBy Trashed WithoutGlobalScopes].freeze
188
-
189
- # Read by other directives rather than resolving anything themselves.
190
- PASSIVE_DEFINITIONS = {
191
- 'spread' => 'directive @spread on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION',
192
- 'inject' => 'directive @inject(context: String!, name: String!) repeatable on FIELD_DEFINITION',
193
- 'cacheKey' => 'directive @cacheKey on FIELD_DEFINITION'
194
- }.freeze
195
+ ARG_BUILDERS = %i[
196
+ Eq Neq In NotIn Like Where WhereKey OrderBy Trashed WithoutGlobalScopes
197
+ WhereNull WhereNotNull WhereBetween WhereNotBetween WhereJsonContains Scope Limit
198
+ ].freeze
195
199
 
196
200
  def register_defaults
197
201
  resolvers = Lighthouse::GraphQL::DirectiveResolvers
@@ -200,9 +204,14 @@ class Lighthouse::DirectiveRegistry
200
204
  ARG_BUILDERS.each { |constant| register(Lighthouse::GraphQL::Directives::Arguments.const_get(constant, false)) }
201
205
 
202
206
  # SDL manipulators run in registration order during the manipulation phase.
207
+ # @hide / @show first, so a removed field generates nothing; then @model,
208
+ # which names the model of fields @paginate then rewrites.
209
+ register_manipulator(Lighthouse::GraphQL::Directives::VisibilityDirective)
210
+ register_manipulator(Lighthouse::GraphQL::Directives::ModelDirective)
203
211
  register_manipulator(Lighthouse::GraphQL::Directives::PaginationManipulator)
204
212
  register_manipulator(Lighthouse::GraphQL::Directives::WhereConditionsDirective)
205
213
  register_manipulator(Lighthouse::GraphQL::Directives::SoftDeletesDirective)
214
+ register_manipulator(Lighthouse::GraphQL::Directives::NodeDirective)
206
215
 
207
216
  register_default_definitions
208
217
  self
@@ -213,16 +222,8 @@ class Lighthouse::DirectiveRegistry
213
222
  # SDL of the directives (and the types they refer to) that no registered
214
223
  # class contributes.
215
224
  def register_default_definitions
216
- register_definition('builder', Lighthouse::GraphQL::DirectiveResolvers::BuilderApplier::DEFINITION)
217
- PASSIVE_DEFINITIONS.each { |name, definition| register_definition(name, definition) }
218
- register_definition('cacheControl', Lighthouse::GraphQL::CacheControl::DEFINITION)
219
- Lighthouse::GraphQL::CacheControl::TYPE_DEFINITIONS.each { |name, sdl| register_type_definition(name, sdl) }
220
- register_definition('feature', Lighthouse::GraphQL::FeatureDirective::DEFINITION)
221
- Lighthouse::GraphQL::FeatureDirective::TYPE_DEFINITIONS.each { |name, sdl| register_type_definition(name, sdl) }
222
- Lighthouse::GraphQL::Directives::WhereConditionsDirective::DEFINITIONS.each do |name, definition|
223
- register_definition(name, definition)
224
- end
225
- Lighthouse::GraphQL::Directives::SoftDeletesDirective::DEFINITIONS.each { |name, definition| register_definition(name, definition) }
225
+ Lighthouse::GraphQL::Directives::Definitions.directives.each { |name, definition| register_definition(name, definition) }
226
+ Lighthouse::GraphQL::Directives::Definitions.types.each { |name, sdl| register_type_definition(name, sdl) }
226
227
  end
227
228
 
228
229
  def safe_definition(klass)
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @hide(env:) and @show(env:) — leave a field out of the schema in some
4
+ # environments, as PHP Lighthouse's directives: @hide removes it in the ones
5
+ # listed, @show keeps it only in those. The environment is Rails.env (see
6
+ # Support::Environment); the SDL is rewritten before the schema is built.
7
+ #
8
+ # type Query {
9
+ # debugInfo: String @show(env: ["development", "test"])
10
+ # legacyFeed: [Post!]! @hide(env: ["production"]) @all
11
+ # }
12
+ class Lighthouse::GraphQL::Directives::VisibilityDirective
13
+ DEFINITIONS = {
14
+ 'hide' => <<~GRAPHQL.chomp,
15
+ """
16
+ Excludes the annotated element from the schema conditionally.
17
+ """
18
+ directive @hide(
19
+ "Specify which environments must not use this field, e.g. [\\"production\\"]."
20
+ env: [String!]!
21
+ ) on FIELD_DEFINITION
22
+ GRAPHQL
23
+ 'show' => <<~GRAPHQL.chomp
24
+ """
25
+ Includes the annotated element in the schema conditionally.
26
+ """
27
+ directive @show(
28
+ "Specify which environments may use this field, e.g. [\\"testing\\"]."
29
+ env: [String!]!
30
+ ) on FIELD_DEFINITION
31
+ GRAPHQL
32
+ }.freeze
33
+
34
+ FIELDS = [
35
+ ::GraphQL::Language::Nodes::ObjectTypeDefinition, ::GraphQL::Language::Nodes::ObjectTypeExtension,
36
+ ::GraphQL::Language::Nodes::InterfaceTypeDefinition, ::GraphQL::Language::Nodes::InterfaceTypeExtension
37
+ ].freeze
38
+
39
+ DECLARATION = /directive\s+@(hide|show)\b/
40
+ USAGE = /@(hide|show)\b/
41
+
42
+ def self.manipulate(sdl)
43
+ return sdl unless sdl.is_a?(String) && sdl.gsub(DECLARATION, '').match?(USAGE)
44
+
45
+ document = ::GraphQL::Language::Parser.parse(sdl)
46
+ env = Lighthouse::Support::Environment.current
47
+ ::GraphQL::Language::Printer.new.print(document.merge(definitions: document.definitions.map { |definition| visible_fields(definition, env) }))
48
+ rescue ::GraphQL::ParseError
49
+ sdl
50
+ end
51
+
52
+ def self.visible_fields(definition, env)
53
+ return definition unless FIELDS.any? { |kind| definition.is_a?(kind) }
54
+
55
+ definition.merge(fields: definition.fields.select { |field| visible?(field, env) })
56
+ end
57
+
58
+ def self.visible?(field, env)
59
+ field.directives.all? do |directive|
60
+ case directive.name
61
+ when 'hide' then environments(directive).exclude?(env)
62
+ when 'show' then environments(directive).include?(env)
63
+ else true
64
+ end
65
+ end
66
+ end
67
+
68
+ def self.environments(directive)
69
+ Array(directive.arguments.find { |argument| argument.name == 'env' }&.value).map(&:to_s)
70
+ end
71
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @enum(value:) — the internal value of an enum value, as PHP Lighthouse's
4
+ # directive: what a resolver gets for it in an argument, and what it answers
5
+ # with in a result.
6
+ #
7
+ # enum Role {
8
+ # ADMIN @enum(value: "admin")
9
+ # MEMBER @enum(value: "member")
10
+ # }
11
+ #
12
+ # `users(role: Role @eq)` then filters on "admin", and a User whose role is
13
+ # "admin" answers ADMIN. Without the directive, a value is its own name.
14
+ module Lighthouse::GraphQL::EnumDirective
15
+ DEFINITION = <<~GRAPHQL.chomp
16
+ """
17
+ Assign an internal value to an enum key.
18
+ When dealing with the Enum type in your code,
19
+ you will receive the defined value instead of the string key.
20
+ """
21
+ directive @enum(
22
+ "The internal value of the enum key."
23
+ value: Mixed
24
+ ) on ENUM_VALUE
25
+ GRAPHQL
26
+
27
+ TYPE_DEFINITIONS = { 'Mixed' => 'scalar Mixed' }.freeze
28
+
29
+ module_function
30
+
31
+ def apply(schema)
32
+ schema.types.each_value { |type| apply_to(type) if type.kind.enum? }
33
+ end
34
+
35
+ def apply_to(enum_type)
36
+ enum_type.values.each_value do |enum_value|
37
+ directive = (enum_value.ast_node&.directives || []).find { |d| d.name == 'enum' }
38
+ enum_value.value(Lighthouse::Support::DirectiveArgs.literal(directive, 'value')) if directive
39
+ end
40
+ end
41
+ end
@@ -33,7 +33,8 @@ module Lighthouse::Events::ExecutionTrace
33
33
  result = query.result_values
34
34
  return unless result.is_a?(Hash)
35
35
 
36
- merge_extensions(result, Lighthouse::Events.dispatch(Lighthouse::Events::BuildExtensionsResponse.new(result, nil)))
36
+ complexity = Lighthouse::GraphQL::Complexity.query_complexity(query)
37
+ merge_extensions(result, Lighthouse::Events.dispatch(Lighthouse::Events::BuildExtensionsResponse.new(result, complexity)))
37
38
  manipulate = Lighthouse::Events::ManipulateResult.new(result)
38
39
  Lighthouse::Events.dispatch(manipulate)
39
40
  result.replace(manipulate.result) unless manipulate.result.equal?(result)
@@ -71,7 +71,9 @@ module Lighthouse::Events
71
71
 
72
72
  # A query ran. A listener may return an Events::ExtensionsResponse (or an
73
73
  # Array of them), which is merged into the result's "extensions".
74
- # `query_complexity` is nil unless it was computed.
74
+ # `query_complexity` is the query's complexity score, as graphql-ruby checks it
75
+ # against max_complexity (see GraphQL::Complexity); nil for a query that
76
+ # failed validation, which is never scored.
75
77
  class BuildExtensionsResponse < Event
76
78
  attr_reader :result, :query_complexity
77
79
 
@@ -3,21 +3,27 @@
3
3
  # Install a field's resolvers innermost first (see
4
4
  # DirectiveRegistry#resolution_order), so each wrapper decorates the ones
5
5
  # before it. Besides its directives, a field gets the conventional
6
- # Queries::/Mutations:: resolver when it is a root field with no resolving
7
- # directive, and argument validation when an input type it takes carries
8
- # @validator. The registry replaces a hardcoded `case dnode.name`: adding a
6
+ # Resolvers::/Mutations:: resolver when it is a root field with no resolving
7
+ # directive, the method of the type's class (Types::PostType) when another
8
+ # type's field has none, and argument validation when an input type it takes
9
+ # carries @validator. The registry replaces a hardcoded `case dnode.name`: adding a
9
10
  # directive is "register a class", not "edit the generator".
10
11
  module Lighthouse::GraphQL::FieldResolverStack
11
12
  module_function
12
13
 
13
- def apply(type, field_def, directives, root_kind)
14
- opts = { root_kind: root_kind }
14
+ # `bases`: { root kind => base class } from the schema's base_types (ResolverBases).
15
+ # `type_class`: the class of a non-root type (Types::PostType), whose methods
16
+ # resolve its fields that have no resolving directive.
17
+ def apply(type, field_def, root_kind, bases: {}, type_class: nil)
18
+ directives = field_def.ast_node.directives || []
19
+ opts = { root_kind: root_kind, bases: bases }
15
20
  steps = directive_steps(type, field_def, directives, opts)
16
- steps << convention_step(type, field_def, root_kind) if root_kind && !resolving_directive?(directives)
21
+ steps << limit_step(type, field_def, opts) if Lighthouse::GraphQL::DirectiveResolvers::LimitResolver.keyword(field_def)
22
+ steps << own_step(type, field_def, root_kind, bases, type_class) unless resolving_directive?(directives)
17
23
  steps << validator_step(type, field_def, opts) if input_validation?(field_def, directives)
18
- steps << empty_strings_step(type, field_def, opts) if empty_string_conversion?(field_def, directives)
24
+ steps.concat(argument_steps(type, field_def, directives, opts))
19
25
 
20
- steps.each_with_index.sort_by { |(order, _), index| [order, index] }.each { |(_, step), _| step.call }
26
+ steps.compact.each_with_index.sort_by { |(order, _), index| [order, index] }.each { |(_, step), _| step.call }
21
27
  end
22
28
 
23
29
  # [order, step] for every directive the registry has a resolver for.
@@ -38,17 +44,33 @@ module Lighthouse::GraphQL::FieldResolverStack
38
44
  directives.any? { |d| registry.resolution_class(d.name) && !registry.wrapper?(d.name) }
39
45
  end
40
46
 
41
- def convention_step(type, field_def, root_kind)
47
+ def convention_step(type, field_def, root_kind, bases = {})
42
48
  resolvers = Lighthouse::GraphQL::DirectiveResolvers
43
49
  resolver =
44
50
  if root_kind == :subscriptions
45
- resolvers::SubscriptionDirectiveResolver.new(field_def, type, nil, root_kind: root_kind)
51
+ resolvers::SubscriptionDirectiveResolver.new(field_def, type, nil, root_kind: root_kind, bases: bases)
46
52
  else
47
- resolvers::ConventionResolver.new(field_def, type, root_kind)
53
+ resolvers::ConventionResolver.new(field_def, type, root_kind, bases[root_kind])
48
54
  end
49
55
  [0, -> { resolver.define_resolver(type) }]
50
56
  end
51
57
 
58
+ # What resolves a field with no resolving directive: the conventional class
59
+ # of a root field, or the method of the type's class. nil: the default reader.
60
+ def own_step(type, field_def, root_kind, bases, type_class)
61
+ return convention_step(type, field_def, root_kind, bases) if root_kind
62
+
63
+ type_method_step(type, field_def, type_class) if type_class
64
+ end
65
+
66
+ # Nothing to install when the class does not define the field's method: the
67
+ # field is read off the object.
68
+ def type_method_step(type, field_def, type_class)
69
+ resolver = Lighthouse::GraphQL::DirectiveResolvers::TypeMethodResolver
70
+ method_name = resolver.method_for(type_class, field_def)
71
+ [0, -> { resolver.new(field_def, type, method_name).define_resolver(type) }] if method_name
72
+ end
73
+
52
74
  # An explicit @validator on the field already validates its arguments.
53
75
  def input_validation?(field_def, directives)
54
76
  directives.none? { |d| d.name == 'validator' } &&
@@ -68,6 +90,32 @@ module Lighthouse::GraphQL::FieldResolverStack
68
90
  Lighthouse::GraphQL::DirectiveResolvers::ConvertEmptyStringsToNullDirectiveResolver.implicit?(field_def)
69
91
  end
70
92
 
93
+ # The steps that change the arguments: empty strings to null and the rewriting
94
+ # directives (@globalId, @trim, @drop) outermost, so every wrapper sees them;
95
+ # the effectful ones (@hash, @bind, @upload) once @guard, @can* and @validator
96
+ # have let the call through (ArgumentTransformsResolver).
97
+ ARGUMENT_PHASES = { rewrite: 'convertEmptyStringsToNull', effects: 'cache' }.freeze
98
+
99
+ def argument_steps(type, field_def, directives, opts)
100
+ steps = []
101
+ steps << empty_strings_step(type, field_def, opts) if empty_string_conversion?(field_def, directives)
102
+ ARGUMENT_PHASES.each do |phase, alongside|
103
+ next unless Lighthouse::GraphQL::DirectiveResolvers::ArgumentTransformsResolver.transforms?(field_def, phase)
104
+
105
+ steps << transforms_step(type, field_def, opts.merge(phase: phase), Lighthouse::DirectiveRegistry.default.resolution_order(alongside))
106
+ end
107
+ steps
108
+ end
109
+
110
+ # Around whatever resolves the field, as @lazyLoad: the result is cut to the client's @limit.
111
+ def limit_step(type, field_def, opts)
112
+ [1, -> { Lighthouse::GraphQL::DirectiveResolvers::LimitResolver.new(field_def, type, nil, opts).define_resolver(type) }]
113
+ end
114
+
115
+ def transforms_step(type, field_def, opts, order)
116
+ [order, -> { Lighthouse::GraphQL::DirectiveResolvers::ArgumentTransformsResolver.new(field_def, type, nil, opts).define_resolver(type) }]
117
+ end
118
+
71
119
  def empty_strings_step(type, field_def, opts)
72
120
  resolver = Lighthouse::GraphQL::DirectiveResolvers::ConvertEmptyStringsToNullDirectiveResolver
73
121
  [Lighthouse::DirectiveRegistry.default.resolution_order(resolver.directive_name),
@@ -25,6 +25,7 @@ module Lighthouse::GraphQL::GeneratedResolvers
25
25
 
26
26
  # Generated resolvers take either (obj, args) or (obj, args, ctx).
27
27
  def invoke(resolver, obj, args, ctx)
28
+ args = take_lookahead(args, ctx)
28
29
  return resolver.call(obj, args) if resolver.parameters.length == 2
29
30
 
30
31
  resolver.call(obj, args, ctx)
@@ -37,13 +38,7 @@ module Lighthouse::GraphQL::GeneratedResolvers
37
38
  def install_dispatcher(schema)
38
39
  return unless schema.respond_to?(:definition_default_resolve=) && schema.respond_to?(:definition_default_resolve)
39
40
 
40
- previous_default = schema.definition_default_resolve
41
- schema.definition_default_resolve = proc do |type, field, obj, arguments, ctx|
42
- resolver = generated_resolver(schema, type, field)
43
- next invoke(resolver, obj, arguments, ctx) if resolver
44
-
45
- previous_default.call(type, field, obj, arguments, ctx)
46
- end
41
+ schema.definition_default_resolve = Lighthouse::GraphQL::DefaultResolveDispatcher.new(schema, schema.definition_default_resolve)
47
42
  end
48
43
 
49
44
  # Our generated resolver for `field`, looked up on the schema's own type
@@ -65,6 +60,16 @@ module Lighthouse::GraphQL::GeneratedResolvers
65
60
  end
66
61
  end
67
62
 
63
+ # The arguments without graphql-ruby's lookahead extra, which a field resolved
64
+ # by a class asks for (ResolveInfo.request_lookahead): it is kept in the
65
+ # field's scoped context, for its ResolveInfo, and no wrapper sees it.
66
+ def take_lookahead(args, ctx)
67
+ return args unless args.is_a?(Hash) && args[:lookahead].is_a?(::GraphQL::Execution::Lookahead)
68
+
69
+ ctx.scoped_set!(Lighthouse::GraphQL::ResolveInfo::LOOKAHEAD, args[:lookahead]) if ctx.respond_to?(:scoped_set!)
70
+ args.except(:lookahead)
71
+ end
72
+
68
73
  # GraphQL-Ruby's SDL builder defines `resolve_<field>` methods that call
69
74
  # the schema's `definition_default_resolve`. Override `resolve_<field>`
70
75
  # to delegate to our generated `resolve_field_<field>` when present, so
@@ -74,7 +79,7 @@ module Lighthouse::GraphQL::GeneratedResolvers
74
79
  return unless type.respond_to?(generated)
75
80
 
76
81
  type.define_singleton_method("resolve_#{field_def.name}") do |obj, arguments, ctx|
77
- public_send(generated, obj, arguments, ctx)
82
+ public_send(generated, obj, Lighthouse::GraphQL::GeneratedResolvers.take_lookahead(arguments, ctx), ctx)
78
83
  end
79
84
  rescue StandardError
80
85
  # ignore override failures