lighthouse-graphql 0.4.0 → 0.6.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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +256 -1
  3. data/README.md +26 -15
  4. data/docs/caching.md +4 -3
  5. data/docs/configuration.md +16 -12
  6. data/docs/directives.md +294 -4
  7. data/docs/generators.md +84 -0
  8. data/docs/getting-started.md +93 -60
  9. data/docs/performance.md +2 -2
  10. data/docs/relationships.md +47 -4
  11. data/docs/relay.md +200 -0
  12. data/docs/resolvers.md +326 -34
  13. data/docs/security.md +73 -8
  14. data/docs/subscriptions.md +16 -9
  15. data/docs/types.md +218 -0
  16. data/docs/uploads.md +107 -0
  17. data/lib/generators/lighthouse/base_classes.rb +27 -49
  18. data/lib/generators/lighthouse/install/graphql_ruby_install.rb +113 -0
  19. data/lib/generators/lighthouse/install/install_generator.rb +125 -45
  20. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +10 -8
  21. data/lib/lighthouse/async_mutation_job.rb +17 -0
  22. data/lib/lighthouse/caching.rb +9 -3
  23. data/lib/lighthouse/complexity.rb +5 -4
  24. data/lib/lighthouse/config.rb +12 -18
  25. data/lib/lighthouse/controller.rb +53 -12
  26. data/lib/lighthouse/dataloader_sources/preload_loader.rb +16 -0
  27. data/lib/lighthouse/dataloader_sources/relation_counts_loader.rb +46 -0
  28. data/lib/lighthouse/default_resolve_dispatcher.rb +26 -0
  29. data/lib/lighthouse/directive_resolvers/all_resolver.rb +1 -1
  30. data/lib/lighthouse/directive_resolvers/argument_binding.rb +61 -0
  31. data/lib/lighthouse/directive_resolvers/argument_transforms_resolver.rb +220 -0
  32. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +2 -2
  33. data/lib/lighthouse/directive_resolvers/async_resolver.rb +90 -0
  34. data/lib/lighthouse/directive_resolvers/base.rb +63 -14
  35. data/lib/lighthouse/directive_resolvers/builder_applier.rb +89 -19
  36. data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +1 -1
  37. data/lib/lighthouse/directive_resolvers/can_resolvers.rb +1 -1
  38. data/lib/lighthouse/directive_resolvers/convention_resolver.rb +2 -1
  39. data/lib/lighthouse/directive_resolvers/count_resolver.rb +1 -1
  40. data/lib/lighthouse/directive_resolvers/field_resolver.rb +35 -7
  41. data/lib/lighthouse/directive_resolvers/first_resolver.rb +1 -1
  42. data/lib/lighthouse/directive_resolvers/global_id_resolver.rb +59 -0
  43. data/lib/lighthouse/directive_resolvers/lazy_load_resolver.rb +61 -0
  44. data/lib/lighthouse/directive_resolvers/limit_resolver.rb +59 -0
  45. data/lib/lighthouse/directive_resolvers/method_resolver.rb +38 -3
  46. data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +1 -1
  47. data/lib/lighthouse/directive_resolvers/namespaced_resolver.rb +31 -0
  48. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +1 -1
  49. data/lib/lighthouse/directive_resolvers/resolution.rb +19 -4
  50. data/lib/lighthouse/directive_resolvers/subscription_resolver.rb +2 -1
  51. data/lib/lighthouse/directive_resolvers/throttle_resolver.rb +136 -0
  52. data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +87 -0
  53. data/lib/lighthouse/directive_resolvers/with_count_resolver.rb +43 -0
  54. data/lib/lighthouse/directive_resolvers/with_resolver.rb +42 -0
  55. data/lib/lighthouse/directives/arguments/more_filters.rb +215 -0
  56. data/lib/lighthouse/directives/definitions.rb +52 -0
  57. data/lib/lighthouse/directives/model_directive.rb +131 -0
  58. data/lib/lighthouse/directives/node_directive.rb +134 -0
  59. data/lib/lighthouse/directives/pagination_manipulator.rb +1 -1
  60. data/lib/lighthouse/directives/registry.rb +21 -21
  61. data/lib/lighthouse/directives/visibility_directive.rb +71 -0
  62. data/lib/lighthouse/enum_directive.rb +41 -0
  63. data/lib/lighthouse/field_resolver_stack.rb +56 -6
  64. data/lib/lighthouse/generated_resolvers.rb +13 -8
  65. data/lib/lighthouse/global_id.rb +57 -0
  66. data/lib/lighthouse/graphql/version.rb +1 -1
  67. data/lib/lighthouse/graphql_ruby_patches.rb +45 -0
  68. data/lib/lighthouse/multipart_request.rb +43 -0
  69. data/lib/lighthouse/node_registry.rb +68 -0
  70. data/lib/lighthouse/pagination.rb +3 -0
  71. data/lib/lighthouse/rb_lighthouse.rb +34 -0
  72. data/lib/lighthouse/relation_counts.rb +29 -0
  73. data/lib/lighthouse/resolve_info.rb +80 -0
  74. data/lib/lighthouse/resolver_bases.rb +25 -4
  75. data/lib/lighthouse/resolver_dispatch.rb +51 -11
  76. data/lib/lighthouse/root_types.rb +117 -0
  77. data/lib/lighthouse/scalar_coercion.rb +72 -0
  78. data/lib/lighthouse/schema_cache.rb +2 -1
  79. data/lib/lighthouse/schema_generator.rb +13 -11
  80. data/lib/lighthouse/schema_implementation.rb +4 -8
  81. data/lib/lighthouse/support/environment.rb +13 -0
  82. data/lib/lighthouse/support/model_resolver.rb +7 -5
  83. data/lib/lighthouse/support/preload.rb +49 -0
  84. data/lib/lighthouse/type.rb +103 -0
  85. data/lib/lighthouse/type_resolution.rb +150 -0
  86. data/lib/lighthouse/upload.rb +28 -0
  87. data/lib/lighthouse/upload_store.rb +57 -0
  88. data/sig/lighthouse/directives.rbs +3 -2
  89. data/sig/lighthouse.rbs +10 -2
  90. metadata +40 -4
  91. data/lib/generators/lighthouse/install/templates/schema.rb.tt +0 -22
  92. data/lib/generators/lighthouse/mutation_generator.rb +0 -31
  93. data/lib/generators/lighthouse/query_generator.rb +0 -30
@@ -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,17 +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
- register_definition('complexity', Lighthouse::GraphQL::Complexity::DEFINITION)
222
- Lighthouse::GraphQL::FeatureDirective::TYPE_DEFINITIONS.each { |name, sdl| register_type_definition(name, sdl) }
223
- Lighthouse::GraphQL::Directives::WhereConditionsDirective::DEFINITIONS.each do |name, definition|
224
- register_definition(name, definition)
225
- end
226
- 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) }
227
227
  end
228
228
 
229
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
@@ -4,21 +4,26 @@
4
4
  # DirectiveRegistry#resolution_order), so each wrapper decorates the ones
5
5
  # before it. Besides its directives, a field gets the conventional
6
6
  # Resolvers::/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
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
14
  # `bases`: { root kind => base class } from the schema's base_types (ResolverBases).
14
- def apply(type, field_def, directives, root_kind, bases: {})
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 || []
15
19
  opts = { root_kind: root_kind, bases: bases }
16
20
  steps = directive_steps(type, field_def, directives, opts)
17
- steps << convention_step(type, field_def, root_kind, bases) 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)
18
23
  steps << validator_step(type, field_def, opts) if input_validation?(field_def, directives)
19
- 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))
20
25
 
21
- 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 }
22
27
  end
23
28
 
24
29
  # [order, step] for every directive the registry has a resolver for.
@@ -50,6 +55,25 @@ module Lighthouse::GraphQL::FieldResolverStack
50
55
  [0, -> { resolver.define_resolver(type) }]
51
56
  end
52
57
 
58
+ # What resolves a field with no resolving directive: the method of the
59
+ # type's class (Types::QueryType#latest_posts too, as in graphql-ruby), else
60
+ # for a Query or Mutation field the conventional class. nil: the default reader.
61
+ def own_step(type, field_def, root_kind, bases, type_class)
62
+ return convention_step(type, field_def, root_kind, bases) if root_kind == :subscriptions
63
+
64
+ step = type_method_step(type, field_def, type_class, root: !root_kind.nil?) if type_class
65
+ step || (convention_step(type, field_def, root_kind, bases) if root_kind)
66
+ end
67
+
68
+ # Nothing to install when the class does not define the field's method: the
69
+ # field is read off the object.
70
+ # `root`: the class is the root type's, which the type is built on, not one found by name.
71
+ def type_method_step(type, field_def, type_class, root: false)
72
+ resolver = Lighthouse::GraphQL::DirectiveResolvers::TypeMethodResolver
73
+ method_name = resolver.method_for(type_class, field_def)
74
+ [0, -> { resolver.new(field_def, type, method_name, root ? type_class : nil).define_resolver(type) }] if method_name
75
+ end
76
+
53
77
  # An explicit @validator on the field already validates its arguments.
54
78
  def input_validation?(field_def, directives)
55
79
  directives.none? { |d| d.name == 'validator' } &&
@@ -69,6 +93,32 @@ module Lighthouse::GraphQL::FieldResolverStack
69
93
  Lighthouse::GraphQL::DirectiveResolvers::ConvertEmptyStringsToNullDirectiveResolver.implicit?(field_def)
70
94
  end
71
95
 
96
+ # The steps that change the arguments: empty strings to null and the rewriting
97
+ # directives (@globalId, @trim, @drop) outermost, so every wrapper sees them;
98
+ # the effectful ones (@hash, @bind, @upload) once @guard, @can* and @validator
99
+ # have let the call through (ArgumentTransformsResolver).
100
+ ARGUMENT_PHASES = { rewrite: 'convertEmptyStringsToNull', effects: 'cache' }.freeze
101
+
102
+ def argument_steps(type, field_def, directives, opts)
103
+ steps = []
104
+ steps << empty_strings_step(type, field_def, opts) if empty_string_conversion?(field_def, directives)
105
+ ARGUMENT_PHASES.each do |phase, alongside|
106
+ next unless Lighthouse::GraphQL::DirectiveResolvers::ArgumentTransformsResolver.transforms?(field_def, phase)
107
+
108
+ steps << transforms_step(type, field_def, opts.merge(phase: phase), Lighthouse::DirectiveRegistry.default.resolution_order(alongside))
109
+ end
110
+ steps
111
+ end
112
+
113
+ # Around whatever resolves the field, as @lazyLoad: the result is cut to the client's @limit.
114
+ def limit_step(type, field_def, opts)
115
+ [1, -> { Lighthouse::GraphQL::DirectiveResolvers::LimitResolver.new(field_def, type, nil, opts).define_resolver(type) }]
116
+ end
117
+
118
+ def transforms_step(type, field_def, opts, order)
119
+ [order, -> { Lighthouse::GraphQL::DirectiveResolvers::ArgumentTransformsResolver.new(field_def, type, nil, opts).define_resolver(type) }]
120
+ end
121
+
72
122
  def empty_strings_step(type, field_def, opts)
73
123
  resolver = Lighthouse::GraphQL::DirectiveResolvers::ConvertEmptyStringsToNullDirectiveResolver
74
124
  [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
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+
5
+ # Relay global ids, as PHP Lighthouse's Base64GlobalId writes them: the type
6
+ # name and the id, joined by a colon, in base64 ("User:1" -> "VXNlcjox").
7
+ # Config.global_id takes another object with `encode(type, id)` and
8
+ # `decode(global_id)` -> [type, id], as PHP Lighthouse lets the GlobalId
9
+ # contract be rebound.
10
+ #
11
+ # A schema that implements graphql-ruby's Relay ids, `id_from_object` and
12
+ # `object_from_id` (graphql-ruby's install writes them, on Rails' GlobalID),
13
+ # has one kind of global id: @globalId encodes an object's own id with
14
+ # `id_from_object`, and decodes with `object_from_id`, as graphql-ruby's
15
+ # Query.node and Node's `id` do.
16
+ module Lighthouse::GraphQL::GlobalId
17
+ module_function
18
+
19
+ def encode(type, id)
20
+ Base64.strict_encode64("#{type}:#{id}")
21
+ end
22
+
23
+ def decode(global_id)
24
+ Base64.strict_decode64(global_id.to_s).split(':', 2)
25
+ rescue ArgumentError
26
+ []
27
+ end
28
+
29
+ # [type, id] for a global id a client sent, through the schema's
30
+ # object_from_id (schema_ids?) or Config.global_id; a
31
+ # GraphQL::ExecutionError when it is not one.
32
+ def decode!(global_id, context = nil)
33
+ type, id = schema_ids?(context&.schema) ? decode_object(global_id, context) : Lighthouse::Config.global_id.decode(global_id)
34
+ return [type, id] if type.present? && !id.nil?
35
+
36
+ raise ::GraphQL::ExecutionError, "#{global_id.inspect} is not a valid global id."
37
+ end
38
+
39
+ # The global id of `object`'s own id (`id`, what a field of `type` resolved)
40
+ # through the schema's id_from_object (schema_ids?); nil for anything else.
41
+ def encode_field(type, object, id, context)
42
+ return unless schema_ids?(context&.schema) && object.respond_to?(:id) && object.id.to_s == id.to_s
43
+
44
+ context.schema.id_from_object(object, type, context)
45
+ end
46
+
47
+ # Does `schema` define graphql-ruby's object_from_id (GraphQL::Schema's own raises)?
48
+ def schema_ids?(schema)
49
+ schema.respond_to?(:object_from_id) && schema.method(:object_from_id).owner != ::GraphQL::Schema.singleton_class
50
+ end
51
+
52
+ # [type, id] of the object the schema's object_from_id finds.
53
+ def decode_object(global_id, context)
54
+ object = context.schema.object_from_id(global_id, context)
55
+ object.nil? ? [] : [object.class.name.demodulize, object.id]
56
+ end
57
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lighthouse
4
4
  module GraphQL
5
- VERSION = '0.4.0'
5
+ VERSION = '0.6.0'
6
6
  end
7
7
  end
@@ -15,4 +15,49 @@ module Lighthouse::GraphQL
15
15
  # skip the fix rather than fail to load (explicit nulls then fail as before).
16
16
  builder = ::GraphQL::Schema::BuildFromDefinition.const_get(:Builder) if ::GraphQL::Schema::BuildFromDefinition.const_defined?(:Builder)
17
17
  builder.prepend(NullDirectiveArguments) if builder && (builder.method_defined?(:args_to_kwargs) || builder.private_method_defined?(:args_to_kwargs))
18
+
19
+ # graphql-ruby builds every SDL object type on base_types[:object], and every
20
+ # interface from base_types[:interface]. RootTypes builds a root type on the
21
+ # app's code-first one (Types::QueryType) instead, and takes the app's Node
22
+ # interface (Types::NodeType) as the SDL's Node.
23
+ module RootTypeBuilder
24
+ def build_object_type(definition, type_resolver, base_type)
25
+ root = Lighthouse::GraphQL::RootTypes.base_for(definition.name)
26
+ return super unless root
27
+
28
+ super(Lighthouse::GraphQL::RootTypes.definition_on(definition, root), type_resolver, root)
29
+ end
30
+
31
+ def build_interface_type(definition, type_resolver, base_type)
32
+ Lighthouse::GraphQL::RootTypes.base_for(definition.name) || super
33
+ end
34
+ end
35
+
36
+ # As above, a graphql-ruby release that reshapes the builder skips the hook:
37
+ # root types are then built on base_types[:object], as before.
38
+ if builder && %i[build_object_type build_interface_type].all? { |name| builder.method_defined?(name) || builder.private_method_defined?(name) }
39
+ builder.prepend(RootTypeBuilder)
40
+ end
41
+
42
+ # The app's schema class inherits Lighthouse's (`class AppSchema <
43
+ # get_schema(...)`), so its Subscription root type is the parent's. A
44
+ # subscriptions backend the subclass `use`s (ActionCableSubscriptions) then
45
+ # finds no root type of the subclass's own, and graphql-ruby adds its
46
+ # subscription extension to no field. Look the root type up through the
47
+ # parent too. Extended onto the schemas Lighthouse builds (SchemaGenerator).
48
+ module InheritedSubscriptionRoot
49
+ def add_subscription_extension_if_necessary
50
+ return super if @subscription_object.is_a?(Class) || !defined?(::GraphQL::Subscriptions::DefaultSubscriptionResolveExtension)
51
+ return unless inherited_subscription_root_pending?
52
+
53
+ @subscription_extension_added = true
54
+ extension = ::GraphQL::Subscriptions::DefaultSubscriptionResolveExtension
55
+ subscription.all_field_definitions.each { |field| field.extension(extension) unless field.extensions.any?(extension) }
56
+ end
57
+
58
+ # A backend, a root type from the parent, and no extension added yet.
59
+ def inherited_subscription_root_pending?
60
+ !defined?(@subscription_extension_added) && subscription.is_a?(Class) && !subscriptions.nil?
61
+ end
62
+ end
18
63
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ # The GraphQL multipart request specification
6
+ # (https://github.com/jaydenseric/graphql-multipart-request-spec), which file
7
+ # uploads use: a multipart form with `operations` (the JSON operation, or a
8
+ # batch of them), `map` (each file's form key => the paths in `operations` it
9
+ # goes to, e.g. { "0": ["variables.avatar"] }) and the files. The controller
10
+ # concern reads it into the operation(s), with every file in place.
11
+ module Lighthouse::GraphQL::MultipartRequest
12
+ module_function
13
+
14
+ # The operation (a Hash) or batch (an Array), with the files in place; nil
15
+ # when the request is not a multipart one.
16
+ def operations(params)
17
+ raw = param(params, 'operations')
18
+ return unless raw
19
+
20
+ operations = parse(raw)
21
+ parse(param(params, 'map') || '{}').each do |key, paths|
22
+ Array(paths).each { |path| place(operations, path.to_s.split('.'), param(params, key)) }
23
+ end
24
+ operations
25
+ end
26
+
27
+ def param(params, key)
28
+ params[key.to_s] || params[key.to_sym]
29
+ end
30
+
31
+ def parse(value)
32
+ value = value.to_unsafe_h if value.respond_to?(:to_unsafe_h)
33
+ value.is_a?(String) ? JSON.parse(value) : value
34
+ end
35
+
36
+ def place(target, segments, file)
37
+ *parents, last = segments
38
+ container = parents.reduce(target) { |node, segment| node.is_a?(Array) ? node[Integer(segment)] : node[segment] }
39
+ raise ArgumentError, "The multipart map names a path that is not in the operations: #{segments.join('.')}" unless container
40
+
41
+ container.is_a?(Array) ? container[Integer(last)] = file : container[last] = file
42
+ end
43
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Resolves Relay's `Query.node(id:)`, which @node adds, as PHP Lighthouse's
4
+ # NodeRegistry: the id is decoded into a type and an id, and the type must be
5
+ # one with @node. Its object comes from, in order:
6
+ #
7
+ # - `@node(resolver: "Class@method")`: the class (method `call` without `@`),
8
+ # looked up as @field looks a class up on a type, called with the id and the
9
+ # context;
10
+ # - `@node(model: "Name")`, or else the type's @model, or else the model named
11
+ # after the type:
12
+ # the record with that primary key, batched across every `node` in the query.
13
+ #
14
+ # It also tells the Node interface which type each object is (resolve_type):
15
+ # `node` hands back the object with the type its id named, so a `resolver:` may
16
+ # return any object, a Hash included, and one record may be two node types.
17
+ class Lighthouse::GraphQL::NodeRegistry
18
+ # What `node` returns: the object, and the type its global id named.
19
+ Found = Struct.new(:type, :object)
20
+
21
+ def self.resolve(_root, args, context, _info = nil)
22
+ type_name, id = args[:id]
23
+ type = context.schema.get_type(type_name.to_s)
24
+ raise ::GraphQL::ExecutionError, "[#{type_name}] is not a type that can be resolved by node." unless node_type?(type)
25
+
26
+ object = find(type, id, context)
27
+ object.nil? ? nil : Found.new(type, object)
28
+ end
29
+
30
+ # [type, object] for what `node` found; nil leaves any other Node value to the default.
31
+ def self.resolve_type(object, _context)
32
+ [object.type, object.object] if object.is_a?(Found)
33
+ end
34
+
35
+ def self.node_type?(type)
36
+ type&.kind&.object? && !directive(type).nil?
37
+ end
38
+
39
+ def self.directive(type)
40
+ (type.ast_node&.directives || []).find { |d| d.name == 'node' }
41
+ end
42
+
43
+ def self.find(type, id, context)
44
+ node = directive(type)
45
+ resolver = Lighthouse::Support::DirectiveArgs.literal(node, 'resolver')
46
+ return call_resolver(resolver, id, context) if resolver
47
+
48
+ model = model_for(type, Lighthouse::Support::DirectiveArgs.literal(node, 'model'))
49
+ raise Lighthouse::DefinitionError, "@node on #{type.graphql_name} finds no model: give it model: or resolver:." unless model
50
+
51
+ context.dataloader.with(::GraphQL::Dataloader::ActiveRecordSource, model).load(model.type_for_attribute(model.primary_key).cast(id))
52
+ end
53
+
54
+ def self.call_resolver(reference, id, context)
55
+ class_name, method = Lighthouse::GraphQL::DirectiveResolvers::FieldDirectiveResolver.reference(reference)
56
+ klass = Lighthouse::GraphQL::DirectiveResolvers::FieldDirectiveResolver.lookup(class_name, nil)
57
+ raise Lighthouse::DefinitionError, "@node(resolver: #{reference.inspect}) names no class." unless klass
58
+
59
+ receiver = Lighthouse::GraphQL::ResolverDispatch.callable_receiver(klass, method || :call)
60
+ receiver.public_send(method || :call, id, context)
61
+ end
62
+
63
+ def self.model_for(type, name)
64
+ return Lighthouse::Support::ModelResolver.constantize(name) if name
65
+
66
+ Lighthouse::GraphQL::Directives::ModelDirective.model_for(type)
67
+ end
68
+ end
@@ -110,6 +110,9 @@ module Lighthouse::GraphQL::Pagination
110
110
  @items = items
111
111
  end
112
112
 
113
+ # The records on the page, for @lazyLoad.
114
+ attr_reader :items
115
+
113
116
  def marshal_dump
114
117
  [info, @items]
115
118
  end