lighthouse-graphql 0.4.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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +139 -1
  3. data/README.md +8 -5
  4. data/docs/configuration.md +26 -2
  5. data/docs/directives.md +287 -4
  6. data/docs/generators.md +82 -0
  7. data/docs/getting-started.md +26 -30
  8. data/docs/relationships.md +42 -0
  9. data/docs/relay.md +137 -0
  10. data/docs/resolvers.md +261 -22
  11. data/docs/security.md +17 -0
  12. data/docs/types.md +219 -0
  13. data/docs/uploads.md +80 -0
  14. data/lib/generators/lighthouse/base_classes.rb +32 -49
  15. data/lib/generators/lighthouse/install/install_generator.rb +83 -24
  16. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +14 -6
  17. data/lib/generators/lighthouse/install/templates/schema.rb.tt +10 -3
  18. data/lib/lighthouse/async_mutation_job.rb +17 -0
  19. data/lib/lighthouse/complexity.rb +5 -4
  20. data/lib/lighthouse/config.rb +13 -2
  21. data/lib/lighthouse/controller.rb +27 -9
  22. data/lib/lighthouse/dataloader_sources/preload_loader.rb +16 -0
  23. data/lib/lighthouse/dataloader_sources/relation_counts_loader.rb +46 -0
  24. data/lib/lighthouse/default_resolve_dispatcher.rb +26 -0
  25. data/lib/lighthouse/directive_resolvers/all_resolver.rb +1 -1
  26. data/lib/lighthouse/directive_resolvers/argument_binding.rb +61 -0
  27. data/lib/lighthouse/directive_resolvers/argument_transforms_resolver.rb +220 -0
  28. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +2 -2
  29. data/lib/lighthouse/directive_resolvers/async_resolver.rb +90 -0
  30. data/lib/lighthouse/directive_resolvers/base.rb +63 -14
  31. data/lib/lighthouse/directive_resolvers/builder_applier.rb +89 -19
  32. data/lib/lighthouse/directive_resolvers/can_resolvers.rb +1 -1
  33. data/lib/lighthouse/directive_resolvers/convention_resolver.rb +2 -1
  34. data/lib/lighthouse/directive_resolvers/count_resolver.rb +1 -1
  35. data/lib/lighthouse/directive_resolvers/field_resolver.rb +35 -7
  36. data/lib/lighthouse/directive_resolvers/first_resolver.rb +1 -1
  37. data/lib/lighthouse/directive_resolvers/global_id_resolver.rb +57 -0
  38. data/lib/lighthouse/directive_resolvers/lazy_load_resolver.rb +61 -0
  39. data/lib/lighthouse/directive_resolvers/limit_resolver.rb +59 -0
  40. data/lib/lighthouse/directive_resolvers/method_resolver.rb +38 -3
  41. data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +1 -1
  42. data/lib/lighthouse/directive_resolvers/namespaced_resolver.rb +31 -0
  43. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +1 -1
  44. data/lib/lighthouse/directive_resolvers/resolution.rb +19 -4
  45. data/lib/lighthouse/directive_resolvers/subscription_resolver.rb +2 -1
  46. data/lib/lighthouse/directive_resolvers/throttle_resolver.rb +77 -0
  47. data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +81 -0
  48. data/lib/lighthouse/directive_resolvers/with_count_resolver.rb +43 -0
  49. data/lib/lighthouse/directive_resolvers/with_resolver.rb +42 -0
  50. data/lib/lighthouse/directives/arguments/more_filters.rb +215 -0
  51. data/lib/lighthouse/directives/definitions.rb +52 -0
  52. data/lib/lighthouse/directives/model_directive.rb +131 -0
  53. data/lib/lighthouse/directives/node_directive.rb +134 -0
  54. data/lib/lighthouse/directives/pagination_manipulator.rb +1 -1
  55. data/lib/lighthouse/directives/registry.rb +21 -21
  56. data/lib/lighthouse/directives/visibility_directive.rb +71 -0
  57. data/lib/lighthouse/enum_directive.rb +41 -0
  58. data/lib/lighthouse/field_resolver_stack.rb +53 -6
  59. data/lib/lighthouse/generated_resolvers.rb +13 -8
  60. data/lib/lighthouse/global_id.rb +31 -0
  61. data/lib/lighthouse/graphql/version.rb +1 -1
  62. data/lib/lighthouse/multipart_request.rb +43 -0
  63. data/lib/lighthouse/node_registry.rb +68 -0
  64. data/lib/lighthouse/pagination.rb +3 -0
  65. data/lib/lighthouse/rb_lighthouse.rb +33 -0
  66. data/lib/lighthouse/relation_counts.rb +29 -0
  67. data/lib/lighthouse/resolve_info.rb +80 -0
  68. data/lib/lighthouse/resolver_bases.rb +9 -2
  69. data/lib/lighthouse/resolver_dispatch.rb +36 -11
  70. data/lib/lighthouse/scalar_coercion.rb +72 -0
  71. data/lib/lighthouse/schema_cache.rb +2 -1
  72. data/lib/lighthouse/schema_generator.rb +11 -10
  73. data/lib/lighthouse/schema_implementation.rb +4 -8
  74. data/lib/lighthouse/support/environment.rb +13 -0
  75. data/lib/lighthouse/support/model_resolver.rb +7 -5
  76. data/lib/lighthouse/support/preload.rb +49 -0
  77. data/lib/lighthouse/type.rb +103 -0
  78. data/lib/lighthouse/type_resolution.rb +150 -0
  79. data/lib/lighthouse/upload.rb +26 -0
  80. data/lib/lighthouse/upload_store.rb +32 -0
  81. data/sig/lighthouse/directives.rbs +3 -2
  82. data/sig/lighthouse.rbs +18 -0
  83. metadata +38 -1
@@ -3,72 +3,55 @@
3
3
  require 'rails/generators'
4
4
  require 'lighthouse-graphql'
5
5
 
6
- # The base class graphql-ruby's convention puts in each namespace:
7
- # Resolvers::BaseResolver, Mutations::BaseMutation and
8
- # Subscriptions::BaseSubscription. The classes Lighthouse generates inherit from
9
- # them, so what every class of a kind shares (a concern, graphql-ruby's base type
10
- # hookups) is written once, in the base. graphql-ruby's `graphql:install` writes
11
- # the first two the same way.
6
+ # The base class graphql-ruby's convention puts in each namespace, which the
7
+ # classes Lighthouse generates inherit from: Resolvers::BaseResolver,
8
+ # Mutations::BaseMutation and Subscriptions::BaseSubscription.
9
+ #
10
+ # graphql-ruby's `graphql:install` writes the first two (and every Types::Base*),
11
+ # and `lighthouse:install` runs it: Lighthouse does not write copies of them. It
12
+ # writes only the base graphql-ruby has none of, BaseSubscription, which
13
+ # includes Lighthouse's subscription concern. A generator that finds a base
14
+ # missing says how to get it.
12
15
  #
13
16
  # A base that already exists is never replaced, not even with --force: it holds
14
- # the app's own code, and one written by graphql-ruby works as it is.
17
+ # the app's own code.
15
18
  module Lighthouse::Generators::BaseClasses
16
- BASES = {
17
- queries: {
18
- name: 'BaseResolver',
19
- source: <<~RUBY
20
- # What every class resolving a Query field shares: Lighthouse resolves
21
- # Query.latestPosts to Resolvers::LatestPosts by convention, and every such
22
- # class inherits from this one. graphql-ruby's `graphql:install` writes the
23
- # same class.
24
- class BaseResolver < GraphQL::Schema::Resolver
25
- end
26
- RUBY
27
- },
28
- mutations: {
29
- name: 'BaseMutation',
30
- source: <<~RUBY
31
- # What every class resolving a Mutation field shares: Lighthouse resolves
32
- # Mutation.publishPost to Mutations::PublishPost by convention, and every such
33
- # class inherits from this one. The SDL declares the arguments, so it is a
34
- # plain GraphQL::Schema::Mutation; the GraphQL::Schema::RelayClassicMutation
35
- # graphql-ruby's `graphql:install` writes (one `input:` argument) works too.
36
- class BaseMutation < GraphQL::Schema::Mutation
37
- end
38
- RUBY
39
- },
40
- subscriptions: {
41
- name: 'BaseSubscription',
42
- source: <<~RUBY
43
- # What every class resolving a Subscription field shares. The concern is PHP
44
- # Lighthouse's model: every subscriber of a field gets every broadcast, and the
45
- # class's `filter?` decides who receives it. A subclass that defines
46
- # graphql-ruby's own `authorized?` or `update` keeps them, and `topic_by
47
- # :arguments` brings back graphql-ruby's exact-argument matching.
48
- class BaseSubscription < GraphQL::Schema::Subscription
49
- include Lighthouse::GraphQL::Subscription
50
- end
51
- RUBY
52
- }
19
+ NAMES = { queries: 'BaseResolver', mutations: 'BaseMutation', subscriptions: 'BaseSubscription' }.freeze
20
+
21
+ # The bases Lighthouse writes itself, graphql-ruby having none.
22
+ SOURCES = {
23
+ subscriptions: <<~RUBY
24
+ # What every class resolving a Subscription field shares. The concern is PHP
25
+ # Lighthouse's model: every subscriber of a field gets every broadcast, and the
26
+ # class's `filter?` decides who receives it. A subclass that defines
27
+ # graphql-ruby's own `authorized?` or `update` keeps them, and `topic_by
28
+ # :arguments` brings back graphql-ruby's exact-argument matching.
29
+ class BaseSubscription < GraphQL::Schema::Subscription
30
+ include Lighthouse::GraphQL::Subscription
31
+ end
32
+ RUBY
53
33
  }.freeze
54
34
 
55
35
  private
56
36
 
57
37
  # The base class name for `kind` (`BaseResolver`), or nil for a kind without one.
58
38
  def base_class_name(kind)
59
- BASES.dig(kind, :name)
39
+ NAMES[kind]
60
40
  end
61
41
 
62
- # Write the base class of `kind` into `namespace`, unless its file exists.
42
+ # Write the base class of `kind` into `namespace` when Lighthouse owns it and
43
+ # it is missing; say how to get one graphql-ruby owns.
63
44
  def write_base_class(kind, namespace)
64
- base = BASES[kind]
65
- return unless base
45
+ return unless base_class_name(kind)
66
46
 
67
47
  path = base_class_path(kind, namespace)
68
48
  if File.exist?(File.join(destination_root, path))
69
49
  say_status :skip, "#{path} already exists", :yellow
50
+ elsif SOURCES[kind]
51
+ create_file path, ruby_source(namespace.split('::'), SOURCES[kind])
70
52
  else
71
- create_file path, ruby_source(namespace.split('::'), base[:source])
53
+ say_status :missing, "#{path}: graphql-ruby writes it — run `bin/rails g lighthouse:install` (which runs graphql:install), " \
54
+ "or write #{namespace}::#{base_class_name(kind)} yourself", :red
72
55
  end
73
56
  end
74
57
 
@@ -1,23 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rails/generators'
4
+ require 'generators/graphql/install_generator'
4
5
  require 'lighthouse-graphql'
5
6
  require_relative '../base_classes'
6
7
 
7
- # `rails generate lighthouse:install` — sets a Rails app up for Lighthouse:
8
- # the initializer, a starter SDL file, the schema class, the base classes of
9
- # resolvers, mutations and subscriptions, a controller and its route, plus the
10
- # graphql-config file IDEs read. Rerunning it skips the route, SDL, base classes
11
- # and .gitignore entries that are already in place.
8
+ # `rails generate lighthouse:install` — sets a Rails app up for Lighthouse, on
9
+ # top of graphql-ruby's own install, which it runs first (`graphql:install
10
+ # --no-relay --skip-graphiql --skip-query-logs`, keeping the files the app has):
11
+ # graphql-ruby writes the base classes every graphql-ruby app has (Types::Base*,
12
+ # Resolvers::BaseResolver, Mutations::BaseMutation), its controller and route.
13
+ # Lighthouse then puts its own over what graphql-ruby wrote in this run:
12
14
  #
13
- # It coexists with graphql-ruby's `graphql:install`, which writes the same
14
- # controller path and route: an existing schema class or controller is kept
15
- # (pass --force to replace it) instead of overwritten, so an app already
16
- # serving a graphql-ruby schema keeps doing so until it is pointed at ours.
15
+ # - the schema class, built from the SDL (graphql-ruby's is code-first);
16
+ # - the controller, with Lighthouse::GraphQL::Controller;
17
+ # - no Types::QueryType / MutationType: the SDL declares the root types;
18
+ # - Mutations::BaseMutation a GraphQL::Schema::Mutation, as the SDL declares a
19
+ # mutation's arguments (graphql-ruby's is a RelayClassicMutation, one `input:`);
20
+ #
21
+ # and adds the initializer, a starter SDL file, Subscriptions::BaseSubscription
22
+ # and the graphql-config file IDEs read. Rerunning it skips what is in place. A
23
+ # schema class or controller the app had before is kept (pass --force to replace
24
+ # it), so an app already serving a graphql-ruby schema keeps doing so until it
25
+ # is pointed at ours.
17
26
  class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
18
27
  include Lighthouse::Generators::BaseClasses
19
28
 
20
29
  CONTROLLER = 'app/controllers/graphql_controller.rb'
30
+ ROOT_TYPES = %w[app/graphql/types/query_type.rb app/graphql/types/mutation_type.rb].freeze
31
+ BASE_MUTATION = 'app/graphql/mutations/base_mutation.rb'
21
32
 
22
33
  source_root File.expand_path('templates', __dir__)
23
34
 
@@ -32,6 +43,13 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
32
43
  class_option :skip_ide, type: :boolean, default: false,
33
44
  desc: 'Skip graphql.config.yml and the .gitignore entries for the IDE helper files'
34
45
 
46
+ # graphql-ruby's install, for the base classes. What it writes that Lighthouse
47
+ # replaces is noted, so a file the app had before is never touched.
48
+ def install_graphql_ruby
49
+ @before = ([schema_path, CONTROLLER, BASE_MUTATION] + ROOT_TYPES).select { |path| exist?(path) }
50
+ invoke 'graphql:install', [], schema: schema_name, relay: false, skip_graphiql: true, skip_query_logs: true, skip: true
51
+ end
52
+
35
53
  def create_initializer
36
54
  template 'initializer.rb.tt', 'config/initializers/lighthouse.rb'
37
55
  end
@@ -46,23 +64,39 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
46
64
  end
47
65
 
48
66
  def create_schema_class
49
- path = File.join('app/graphql', "#{schema_name.underscore}.rb")
50
- return if keep_existing?(path)
67
+ return if keep_existing?(schema_path)
68
+
69
+ template 'schema.rb.tt', schema_path, force: true
70
+ end
51
71
 
52
- template 'schema.rb.tt', path
72
+ # The SDL declares the root types: graphql-ruby's code-first ones go. Not when
73
+ # the app keeps a code-first schema of its own, which graphql-ruby's install
74
+ # pointed at them.
75
+ def remove_root_types
76
+ return unless exist?(schema_path) && File.read(File.join(destination_root, schema_path)).include?('RbLightHouse.get_schema')
77
+
78
+ ROOT_TYPES.each { |path| remove_file path unless before?(path) }
53
79
  end
54
80
 
55
- # Resolvers::BaseResolver, Mutations::BaseMutation and Subscriptions::BaseSubscription,
56
- # in the namespaces Config.namespaces names first (see BaseClasses).
57
- def create_base_classes
58
- BASES.each_key { |kind| write_base_class(kind, Lighthouse::Config.namespaces[kind].first) }
81
+ # The SDL declares a mutation's arguments, so its class is a plain
82
+ # GraphQL::Schema::Mutation, not graphql-ruby's RelayClassicMutation.
83
+ def adapt_base_mutation
84
+ return if before?(BASE_MUTATION) || !exist?(BASE_MUTATION)
85
+
86
+ gsub_file BASE_MUTATION, 'GraphQL::Schema::RelayClassicMutation', 'GraphQL::Schema::Mutation'
87
+ gsub_file BASE_MUTATION, /^\s*input_object_class .*\n/, ''
88
+ end
89
+
90
+ # Subscriptions::BaseSubscription, which graphql-ruby has none of (see BaseClasses).
91
+ def create_base_subscription
92
+ write_base_class(:subscriptions, Lighthouse::Config.namespaces[:subscriptions].first)
59
93
  end
60
94
 
61
95
  def create_controller
62
- return if options[:skip_controller]
96
+ return remove_graphql_ruby_controller if options[:skip_controller]
63
97
  return if (@kept_controller = keep_existing?(CONTROLLER))
64
98
 
65
- template 'graphql_controller.rb.tt', CONTROLLER
99
+ template 'graphql_controller.rb.tt', CONTROLLER, force: true
66
100
  end
67
101
 
68
102
  def add_route
@@ -104,23 +138,48 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
104
138
  Lighthouse is installed. Next steps:
105
139
  1. Describe your API in #{sdl_folder}/*.graphql (the starter schema assumes a User model).
106
140
  2. Resolve a field in Ruby when a directive is not enough: bin/rails g lighthouse:query LatestPosts
107
- (its class inherits from Resolvers::BaseResolver; mutations and subscriptions have a base too)
108
- 3. Generate the IDE helper files: bin/rails lighthouse:ide_helper
109
- 4. #{serve}
141
+ (it inherits from Resolvers::BaseResolver, which graphql-ruby's install wrote; lighthouse:mutation and
142
+ lighthouse:subscription write the others)
143
+ 3. Give a type Ruby methods, a union or interface its resolve_type, a scalar its coercion:
144
+ bin/rails g graphql:object Post (or graphql:union, graphql:interface, graphql:scalar),
145
+ then include Lighthouse::GraphQL::Type in the class
146
+ 4. Generate the IDE helper files: bin/rails lighthouse:ide_helper
147
+ 5. #{serve}
110
148
  MSG
111
149
  end
112
150
 
113
151
  private
114
152
 
115
- # Files another generator may own (graphql-ruby's install writes the same
116
- # controller) are left alone unless --force is given.
153
+ # A file the app had before this run is left alone unless --force is given;
154
+ # one graphql-ruby's install wrote in this run is Lighthouse's to replace.
117
155
  def keep_existing?(path)
118
- return false if options[:force] || !File.exist?(File.join(destination_root, path))
156
+ return false if options[:force] || !before?(path)
119
157
 
120
158
  say_status :skip, "#{path} already exists (pass --force to replace it)", :yellow
121
159
  true
122
160
  end
123
161
 
162
+ def before?(path)
163
+ @before.include?(path)
164
+ end
165
+
166
+ def exist?(path)
167
+ File.exist?(File.join(destination_root, path))
168
+ end
169
+
170
+ def schema_path
171
+ File.join('app/graphql', "#{schema_name.underscore}.rb")
172
+ end
173
+
174
+ # --skip-controller: the app serves the schema itself, so graphql-ruby's
175
+ # controller and route, if its install just wrote them, go.
176
+ def remove_graphql_ruby_controller
177
+ return if before?(CONTROLLER)
178
+
179
+ remove_file CONTROLLER
180
+ gsub_file 'config/routes.rb', %r{^\s*post "/graphql", to: "graphql#execute"\n}, '' if exist?('config/routes.rb')
181
+ end
182
+
124
183
  def schema_name
125
184
  options[:schema].camelize
126
185
  end
@@ -17,21 +17,29 @@ Lighthouse.configure do |config|
17
17
  # come from :builders and @validator contracts from :validators. A Subscription
18
18
  # field resolves to Subscriptions::PostUpdated (a Subscriptions::BaseSubscription),
19
19
  # @event(dispatch:) classes come from :events, and the graphql-ruby analyzers
20
- # @complexity(resolver:) names from :analyzers. Generate them with
21
- # `rails g lighthouse:query|mutation|subscription|builder|validator Name`.
20
+ # @complexity(resolver:) names from :analyzers. Post.readingTime, with no
21
+ # resolving directive, resolves with Types::PostType#reading_time (a
22
+ # Types::BaseObject including Lighthouse::GraphQL::Type), from :types. Generate them with
23
+ # `rails g lighthouse:query|mutation|subscription|type|builder|validator Name`.
22
24
  # config.namespaces = {
23
25
  # queries: ['Resolvers'], mutations: ['Mutations'], subscriptions: ['Subscriptions'],
24
- # builders: ['Builders'], validators: ['Validators'], events: ['Events'], analyzers: ['Analyzers']
26
+ # builders: ['Builders'], validators: ['Validators'], events: ['Events'], analyzers: ['Analyzers'],
27
+ # types: ['Types']
25
28
  # }
26
29
 
27
30
  # Namespaces tried, in order, for models (@all(model: "User")) and for
28
- # @field(resolver:) on types other than Query and Mutation. Fully-qualified
29
- # names (containing `::`) are used as-is.
31
+ # @field(resolver:) on types other than Query and Mutation, after :queries
32
+ # (@field(resolver: "Posts::TagsCount") is Resolvers::Posts::TagsCount).
33
+ # Fully-qualified names (containing `::`) are used as-is.
30
34
  # config.resolver_namespaces = ['App::Graphql']
31
35
 
32
- # Store for @cache and @clearCache.
36
+ # Store for @cache, @clearCache and @throttle's counts.
33
37
  # config.cache_store = Rails.cache
34
38
 
39
+ # Where @upload stores a file: call(file, disk:, path:, public:) returns what the
40
+ # argument holds. By default an ActiveStorage blob, and its signed id.
41
+ # config.upload_store = Lighthouse::GraphQL::UploadStore
42
+
35
43
  # Run @create/@update/@createMany/@updateMany in a database transaction.
36
44
  # config.transactional_mutations = true
37
45
 
@@ -4,15 +4,22 @@
4
4
  # Dataloader batches relationship loads (@hasMany, @belongsTo, ...) to avoid N+1.
5
5
  # base_types names the base classes: every class a Query, Mutation or Subscription
6
6
  # field resolves to must inherit from its kind's, and a Subscription field with no
7
- # class of its own resolves with the subscription one.
7
+ # class of its own resolves with the subscription one. graphql-ruby builds the
8
+ # SDL's types from the others (written by `rails g graphql:install`), and type
9
+ # classes (Types::PostType, Types::DateTimeType, ...) inherit from them.
8
10
  <%= schema_name %> = Lighthouse::GraphQL::RbLightHouse.get_schema(
9
11
  options: {
10
12
  using: { GraphQL::Dataloader => {} },
11
13
  base_types: {
12
14
  resolver: <%= base_class_constant(:queries) %>,
13
15
  mutation: <%= base_class_constant(:mutations) %>,
14
- subscription: <%= base_class_constant(:subscriptions) %>
15
- # object: Types::BaseObject, ... # graphql-ruby's base classes of the SDL's types
16
+ subscription: <%= base_class_constant(:subscriptions) %>,
17
+ object: Types::BaseObject,
18
+ interface: Types::BaseInterface,
19
+ union: Types::BaseUnion,
20
+ scalar: Types::BaseScalar,
21
+ enum: Types::BaseEnum,
22
+ input_object: Types::BaseInputObject
16
23
  }
17
24
  }
18
25
  )
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_job'
4
+
5
+ # Runs what resolves an @async mutation field, from a queue. Loaded on first
6
+ # use — the activejob gem is optional — so it is autoloaded, as BroadcastJob.
7
+ # The schema is passed by name; the field's resolver runs with the arguments
8
+ # and context values the request serialized, inside the schema's Dataloader.
9
+ class Lighthouse::GraphQL::AsyncMutationJob < ActiveJob::Base # rubocop:disable Rails/ApplicationJob
10
+ def perform(schema_name, type_name, field_name, arguments, context_values)
11
+ schema = schema_name.constantize
12
+ type = schema.get_type(type_name)
13
+ inner = Lighthouse::GraphQL::DirectiveResolvers::AsyncDirectiveResolver.inner(type, field_name)
14
+ context = ::GraphQL::Query.new(schema, '{ __typename }', context: context_values.merge(lighthouse_async: true)).context
15
+ context.dataloader.run_isolated { inner.call(nil, arguments.transform_keys(&:to_sym), context) }
16
+ end
17
+ end
@@ -151,11 +151,12 @@ module Lighthouse::GraphQL::Complexity
151
151
 
152
152
  # The class @field names, looked up as @field looks it up.
153
153
  def field_class_lookup(directive, kind)
154
- class_name = %w[resolver class].filter_map { |name| Lighthouse::Support::DirectiveArgs.literal(directive, name) }.first
155
- return unless class_name
154
+ reference = %w[resolver class].filter_map { |name| Lighthouse::Support::DirectiveArgs.literal(directive, name) }.first
155
+ return unless reference
156
156
 
157
- namespaces = kind ? Lighthouse::Config.namespaces[kind] : Lighthouse::Config.resolver_namespaces
158
- -> { Lighthouse::Support::ModelResolver.constantize(class_name, namespaces: namespaces) }
157
+ class_name, = Lighthouse::GraphQL::DirectiveResolvers::FieldDirectiveResolver.reference(reference)
158
+
159
+ -> { Lighthouse::GraphQL::DirectiveResolvers::FieldDirectiveResolver.lookup(class_name, kind) }
159
160
  end
160
161
 
161
162
  # Scores each query as graphql-ruby's MaxQueryComplexity scores it against
@@ -20,7 +20,8 @@ module Lighthouse
20
20
  validators: ['Validators'],
21
21
  events: ['Events'],
22
22
  subscriptions: ['Subscriptions'],
23
- analyzers: ['Analyzers']
23
+ analyzers: ['Analyzers'],
24
+ types: ['Types']
24
25
  }.freeze
25
26
 
26
27
  # Every setting reset! forgets, so the next read falls back to its default.
@@ -32,13 +33,14 @@ module Lighthouse
32
33
  convert_empty_strings_to_null
33
34
  soft_deletes
34
35
  queue_broadcasts broadcast_queue
36
+ global_id upload_store
35
37
  ].freeze
36
38
 
37
39
  class << self
38
40
  attr_writer :sdl_folder, :resolver_namespaces, :on_error, :authenticated_user, :policy_user, :authorizer, :cache_store,
39
41
  :transactional_mutations, :schema_cache, :watch_sdl, :persisted_queries, :persisted_queries_store, :persisted_queries_only,
40
42
  :queue_broadcasts,
41
- :convert_empty_strings_to_null, :feature_checker, :feature_actor
43
+ :convert_empty_strings_to_null, :feature_checker, :feature_actor, :global_id, :upload_store
42
44
 
43
45
  def sdl_folder
44
46
  @sdl_folder ||= (defined?(Rails) && Rails.respond_to?(:root) ? Rails.root.join('graphql').to_s : nil)
@@ -104,6 +106,15 @@ module Lighthouse
104
106
  # when the schema is built, so set it in an initializer.
105
107
  def convert_empty_strings_to_null = @convert_empty_strings_to_null || false
106
108
 
109
+ # Encodes and decodes Relay global ids (@globalId, @node): `encode(type, id)`
110
+ # and `decode(global_id)` -> [type, id]. Base64 of "Type:id" by default.
111
+ def global_id = @global_id || Lighthouse::GraphQL::GlobalId
112
+
113
+ # Stores the file an @upload argument carries: `call(file, disk:, path:,
114
+ # public:)` returns what the argument holds instead. An ActiveStorage blob's
115
+ # signed id by default.
116
+ def upload_store = @upload_store || Lighthouse::GraphQL::UploadStore
117
+
107
118
  def persisted_queries_store
108
119
  @persisted_queries_store || cache_store
109
120
  end
@@ -19,7 +19,8 @@ require 'json'
19
19
  #
20
20
  # `execute` dispatches StartRequest, reads the variables (a JSON String, a Hash
21
21
  # or parameters), runs the operation through PersistedQueries.execute (or
22
- # PersistedQueries.multiplex for a batch, a JSON array body), sets the
22
+ # PersistedQueries.multiplex for a batch, a JSON array body; a multipart
23
+ # request's operations, with its files, as MultipartRequest reads them), sets the
23
24
  # Cache-Control header the selected fields' @cacheControl hints allow, renders
24
25
  # the JSON and dispatches EndRequest. In development an error renders as JSON.
25
26
  #
@@ -46,11 +47,11 @@ module Lighthouse::GraphQL::Controller
46
47
 
47
48
  def execute
48
49
  lighthouse_request do
49
- batch = params[:_json]
50
- if batch.is_a?(Array)
51
- render json: lighthouse_batch(batch)
50
+ operations = lighthouse_operations
51
+ if operations.is_a?(Array)
52
+ render json: lighthouse_batch(operations.map { |operation| lighthouse_hash(operation) })
52
53
  else
53
- result = lighthouse_operation
54
+ result = lighthouse_operation(operations)
54
55
  Lighthouse::GraphQL::CacheControl.apply(response, result)
55
56
  render json: result
56
57
  end
@@ -80,6 +81,11 @@ module Lighthouse::GraphQL::Controller
80
81
  respond_to?(:current_user, true) ? { current_user: current_user } : {}
81
82
  end
82
83
 
84
+ # graphql_context, with the client's IP (for @throttle) unless it sets its own.
85
+ def lighthouse_context
86
+ { remote_ip: request.remote_ip }.merge(graphql_context)
87
+ end
88
+
83
89
  def graphql_schema
84
90
  source = lighthouse_schema_source
85
91
  case source
@@ -103,11 +109,23 @@ module Lighthouse::GraphQL::Controller
103
109
  end
104
110
  end
105
111
 
106
- def lighthouse_operation
112
+ # A multipart request's operation(s), a batch (a JSON array body), or the parameters.
113
+ def lighthouse_operations
114
+ Lighthouse::GraphQL::MultipartRequest.operations(params) || params[:_json] || params
115
+ end
116
+
117
+ # Parameters, or a Hash from a multipart request's JSON, read by key either way.
118
+ def lighthouse_hash(operation)
119
+ (operation.respond_to?(:to_unsafe_h) ? operation.to_unsafe_h : operation.to_h).with_indifferent_access
120
+ end
121
+
122
+ # `operation`: the request's parameters, or a multipart request's operation.
123
+ def lighthouse_operation(operation = params)
124
+ operation = operation.with_indifferent_access if operation.is_a?(Hash)
107
125
  Lighthouse::GraphQL::PersistedQueries.execute(
108
126
  graphql_schema,
109
- query: params[:query], extensions: params[:extensions],
110
- variables: graphql_variables(params[:variables]), operation_name: params[:operationName], context: graphql_context
127
+ query: operation[:query], extensions: operation[:extensions],
128
+ variables: graphql_variables(operation[:variables]), operation_name: operation[:operationName], context: lighthouse_context
111
129
  )
112
130
  end
113
131
 
@@ -116,7 +134,7 @@ module Lighthouse::GraphQL::Controller
116
134
  def lighthouse_batch(operations)
117
135
  operations = operations.map do |operation|
118
136
  { query: operation[:query], extensions: operation[:extensions], variables: graphql_variables(operation[:variables]),
119
- operation_name: operation[:operationName], context: graphql_context }
137
+ operation_name: operation[:operationName], context: lighthouse_context }
120
138
  end
121
139
  Lighthouse::GraphQL::PersistedQueries.multiplex(graphql_schema, operations)
122
140
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Eager-loads associations into every record the Dataloader collected, of
4
+ # every parent, in one query per association: @with and @lazyLoad.
5
+ class Lighthouse::GraphQL::DataloaderSources::PreloadLoader < GraphQL::Dataloader::Source
6
+ def initialize(associations, scopes)
7
+ super()
8
+ @associations = associations
9
+ @scopes = scopes
10
+ end
11
+
12
+ # keys: the records
13
+ def fetch(records)
14
+ Lighthouse::Support::Preload.records(records, @associations, @scopes)
15
+ end
16
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ # How many related records each parent has, for @withCount: every parent the
4
+ # Dataloader collected in one GROUP BY query when the relation can be batched
5
+ # (RelationBatch), else one COUNT per parent. Each count is kept on its parent
6
+ # as `<relation>_count` (RelationCounts).
7
+ class Lighthouse::GraphQL::DataloaderSources::RelationCountsLoader < GraphQL::Dataloader::Source
8
+ # Applies the directive's scopes to the related records, as RelationBatch
9
+ # applies a field's filters. A value, so equal scopes batch together.
10
+ Scopes = Struct.new(:names) do
11
+ def decorate_relation(relation, _args, _context)
12
+ Array(names).reduce(relation) { |scoped, scope| scoped.public_send(scope) }
13
+ end
14
+ end
15
+
16
+ def initialize(relation, scopes)
17
+ super()
18
+ @relation = relation.to_s
19
+ @scopes = Scopes.new(Array(scopes))
20
+ end
21
+
22
+ # keys: the parent records, of one class or several
23
+ def fetch(parents)
24
+ counts = {}.compare_by_identity
25
+ parents.group_by(&:class).each_value { |group| counts.merge!(counts_for(group)) }
26
+ parents.map do |parent|
27
+ Lighthouse::GraphQL::RelationCounts.write(parent, @relation, counts[parent])
28
+ counts[parent]
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ # { parent => count } for parents of one class.
35
+ def counts_for(parents)
36
+ batch = Lighthouse::GraphQL::RelationBatch.for(parents.first, @relation, {}, @scopes, nil)
37
+ return parents.index_with { |parent| own_count(parent) } unless batch
38
+
39
+ counts = batch.counts(parents)
40
+ parents.index_with { |parent| counts.fetch(batch.owner_key(parent), 0) }
41
+ end
42
+
43
+ def own_count(parent)
44
+ @scopes.decorate_relation(parent.public_send(@relation), nil, nil).count
45
+ end
46
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The schema's default resolve while Lighthouse runs it. graphql-ruby asks that
4
+ # one object for every field, for the type of a union's or interface's value
5
+ # (`resolve_type`) and for scalar coercion: the dispatcher answers fields with
6
+ # our generated resolvers, and hands everything else to the implementation it
7
+ # replaced (SchemaImplementation).
8
+ class Lighthouse::GraphQL::DefaultResolveDispatcher
9
+ def initialize(schema, previous)
10
+ @schema = schema
11
+ @previous = previous
12
+ end
13
+
14
+ def call(type, field, obj, arguments, ctx)
15
+ resolver = Lighthouse::GraphQL::GeneratedResolvers.generated_resolver(@schema, type, field)
16
+ return Lighthouse::GraphQL::GeneratedResolvers.invoke(resolver, obj, arguments, ctx) if resolver
17
+
18
+ @previous.call(type, field, obj, arguments, ctx)
19
+ end
20
+
21
+ def resolve_type(abstract_type, obj, ctx) = @previous.resolve_type(abstract_type, obj, ctx)
22
+
23
+ def coerce_input(type, value, ctx) = @previous.coerce_input(type, value, ctx)
24
+
25
+ def coerce_result(type, value, ctx) = @previous.coerce_result(type, value, ctx)
26
+ end
@@ -27,7 +27,7 @@ class Lighthouse::GraphQL::DirectiveResolvers::AllDirectiveResolver < Lighthouse
27
27
 
28
28
  relation = Lighthouse::GraphQL::DirectiveResolvers::BuilderApplier.apply(relation, @field_def.ast_node, args, ctx, self)
29
29
 
30
- relation = apply_where_filters(relation, args)
30
+ relation = apply_where_filters(relation, args, ctx)
31
31
  relation = relation.limit(args[:first]) if args[:first]
32
32
  relation = relation.offset(args[:offset]) if args[:offset]
33
33
  relation
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ # What @bind(class:) puts in place of an argument's value: the record of a model
4
+ # whose `column` matches it (a list's records, for a list), eager-loading `with`;
5
+ # or what a class's `call(value, context)` returns. A value that matches nothing
6
+ # is a validation error on the argument, as PHP Lighthouse's, unless `optional`.
7
+ class Lighthouse::GraphQL::DirectiveResolvers::ArgumentBinding
8
+ def initialize(directive, argument, field_def)
9
+ @directive = directive
10
+ @argument = argument
11
+ @field_def = field_def
12
+ end
13
+
14
+ def bind(value, ctx)
15
+ klass = Lighthouse::Support::ModelResolver.constantize(arg('class'))
16
+ raise Lighthouse::DefinitionError, "@bind(class: #{arg('class').inspect}) on #{@argument.name} names no class." unless klass
17
+ return call(klass, value, ctx) unless model?(klass)
18
+
19
+ value.is_a?(Array) ? records(klass, value) : record(klass, value)
20
+ end
21
+
22
+ private
23
+
24
+ def arg(name)
25
+ Lighthouse::Support::DirectiveArgs.literal(@directive, name)
26
+ end
27
+
28
+ def model?(klass)
29
+ defined?(::ActiveRecord::Base) && klass.is_a?(Class) && klass < ::ActiveRecord::Base
30
+ end
31
+
32
+ def call(klass, value, ctx)
33
+ bound = (klass.respond_to?(:call) ? klass : klass.new).call(value, ctx)
34
+ bound.nil? ? missing : bound
35
+ end
36
+
37
+ def scope(klass)
38
+ with = Array(arg('with'))
39
+ with.empty? ? klass.all : klass.preload(*Lighthouse::Support::Preload.associations(with))
40
+ end
41
+
42
+ def record(klass, value)
43
+ scope(klass).find_by(column => value) || missing
44
+ end
45
+
46
+ def records(klass, values)
47
+ found = scope(klass).where(column => values).index_by { |record| record.public_send(column).to_s }
48
+ bound = values.map { |value| found[value.to_s] }
49
+ bound.include?(nil) ? missing(bound) : bound
50
+ end
51
+
52
+ def column
53
+ arg('column') || 'id'
54
+ end
55
+
56
+ def missing(partial = nil)
57
+ return partial if arg('optional')
58
+
59
+ raise Lighthouse::ValidationError.new(@field_def.name, @argument.name => ["The selected #{@argument.name} is invalid."])
60
+ end
61
+ end