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
@@ -1,37 +1,71 @@
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'
7
+ require_relative 'graphql_ruby_install'
6
8
 
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.
9
+ # `rails generate lighthouse:install` — puts Lighthouse on top of graphql-ruby's
10
+ # own install (`rails g graphql:install`, with whichever of its options the app
11
+ # chose: --relay, --batch, --skip-mutation-root-type, --schema, --directory,
12
+ # ...). When the app has none, it runs graphql:install first, with its
13
+ # defaults. It then adapts what graphql-ruby wrote, instead of writing its own:
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 (`class AppSchema < GraphQL::Schema`) becomes Lighthouse's,
16
+ # built from the SDL on everything graphql-ruby wrote: its base types, base
17
+ # resolver and base mutation, and its code-first Types::QueryType /
18
+ # MutationType, which the SDL's root types extend, so their `field`s (Relay's
19
+ # `node` / `nodes`, what `graphql:mutation` adds) stay. Its Relay
20
+ # Types::NodeType is the SDL's Node. Everything else in the class is
21
+ # graphql-ruby's and stays, but the `resolve_type` stub (Lighthouse resolves
22
+ # types) and the root types it is built on;
23
+ # - the example `test_field` goes from the root types;
24
+ # - graphql-ruby's base object, interface, union and scalar include
25
+ # Lighthouse::GraphQL::Type, so every type class (Types::QueryType,
26
+ # Types::NodeType, what `graphql:object` writes) is Lighthouse's;
27
+ # - with --batch, the schema uses GraphQL::Dataloader too, which Lighthouse's
28
+ # directives batch through;
29
+ # - graphql-ruby's controller, as its install wrote it, becomes one with
30
+ # Lighthouse::GraphQL::Controller.
31
+ #
32
+ # It adds the initializer, a starter SDL file and the graphql-config file IDEs
33
+ # read, and with --subscriptions, Subscriptions::BaseSubscription. Rerunning it
34
+ # skips what is in place.
17
35
  class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
18
36
  include Lighthouse::Generators::BaseClasses
37
+ include Lighthouse::Generators::GraphqlRubyInstall
19
38
 
20
39
  CONTROLLER = 'app/controllers/graphql_controller.rb'
21
40
 
22
41
  source_root File.expand_path('templates', __dir__)
23
42
 
24
- desc 'Install Lighthouse: initializer, starter schema, schema class, base classes, controller, route and IDE config.'
43
+ desc "Install Lighthouse on graphql-ruby's install: schema class, base classes, controller, initializer, starter SDL and IDE config."
25
44
 
26
- class_option :schema, type: :string, default: 'AppSchema',
27
- desc: 'Name of the schema constant to generate'
45
+ class_option :schema, type: :string,
46
+ desc: "The schema class to adopt; for graphql:install, when it runs, its name (default: graphql-ruby's)"
28
47
  class_option :sdl_folder, type: :string, default: 'graphql',
29
48
  desc: 'Folder, relative to the app root, holding the *.graphql files'
49
+ class_option :subscriptions, type: :boolean, default: false,
50
+ desc: 'Add Subscriptions::BaseSubscription, the base of the classes Subscription fields resolve to'
30
51
  class_option :skip_controller, type: :boolean, default: false,
31
- desc: 'Skip the GraphqlController and its route'
52
+ desc: "Leave graphql-ruby's GraphqlController as it is"
32
53
  class_option :skip_ide, type: :boolean, default: false,
33
54
  desc: 'Skip graphql.config.yml and the .gitignore entries for the IDE helper files'
34
55
 
56
+ # graphql-ruby's install: the app's, or one now, with graphql-ruby's defaults.
57
+ def install_graphql_ruby
58
+ return if schema_path
59
+
60
+ say_status :invoke, 'graphql:install (no graphql-ruby schema found)', :blue
61
+ invoke 'graphql:install', [], graphql_install_options
62
+ @schema_path = nil
63
+ return if schema_path
64
+
65
+ raise Thor::Error, "No graphql-ruby schema class (class #{options[:schema] || 'AppSchema'} < GraphQL::Schema) under app/: " \
66
+ 'run `bin/rails g graphql:install` first.'
67
+ end
68
+
35
69
  def create_initializer
36
70
  template 'initializer.rb.tt', 'config/initializers/lighthouse.rb'
37
71
  end
@@ -45,24 +79,62 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
45
79
  template 'schema.graphql.tt', File.join(sdl_folder, 'schema.graphql')
46
80
  end
47
81
 
48
- def create_schema_class
49
- path = File.join('app/graphql', "#{schema_name.underscore}.rb")
50
- return if keep_existing?(path)
82
+ # Subscriptions::BaseSubscription, which graphql-ruby has none of (see BaseClasses).
83
+ def create_base_subscription
84
+ write_base_class(:subscriptions, Lighthouse::Config.namespaces[:subscriptions].first) if options[:subscriptions]
85
+ end
51
86
 
52
- template 'schema.rb.tt', path
87
+ # graphql-ruby's schema class, built by Lighthouse from the SDL on its classes.
88
+ def adopt_schema_class
89
+ source = read(schema_path)
90
+ if lighthouse?(source)
91
+ say_status :skip, "#{schema_path} is Lighthouse's already", :yellow
92
+ return
93
+ end
94
+
95
+ declaration = /^([ \t]*)class (\S+) < GraphQL::Schema[ \t]*\n/
96
+ indent, name = source.match(declaration).captures
97
+ gsub_file schema_path, declaration, "#{indent}#{schema_comment(indent)}#{indent}class #{name} < #{schema_superclass(indent)}\n"
98
+ gsub_file schema_path, /^[ \t]*(query|mutation|subscription)\(Types::(Query|Mutation|Subscription)Type\)\n/, ''
99
+ gsub_file schema_path, /^(#{indent}\)\n)\n+/, '\\1'
100
+ gsub_file schema_path, RESOLVE_TYPE_STUB, "\n"
101
+ add_dataloader if source.include?('use GraphQL::Batch') && source.exclude?('use GraphQL::Dataloader')
53
102
  end
54
103
 
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) }
104
+ # The example field goes; the root types' own fields stay.
105
+ def adapt_root_types
106
+ %w[query_type mutation_type subscription_type].each do |name|
107
+ path = types_path("#{name}.rb")
108
+ gsub_file path, TEST_FIELD, "\n" if exist?(path) && read(path).match?(TEST_FIELD)
109
+ end
59
110
  end
60
111
 
61
- def create_controller
62
- return if options[:skip_controller]
63
- return if (@kept_controller = keep_existing?(CONTROLLER))
112
+ # Every type class is Lighthouse's: graphql-ruby's base object, interface,
113
+ # union and scalar include Lighthouse::GraphQL::Type, so the classes that
114
+ # inherit from them (Types::QueryType, Types::NodeType, what `graphql:object`
115
+ # writes) lend their methods to the SDL's types of their name.
116
+ def adapt_type_bases
117
+ TYPE_CONCERN_ANCHORS.each do |name, anchor|
118
+ path = types_path("#{name}.rb")
119
+ next if !exist?(path) || read(path).include?('Lighthouse::GraphQL::Type')
120
+
121
+ indent = read(path)[anchor, 1]
122
+ next unless indent
123
+
124
+ indent += ' ' unless name == 'base_interface'
125
+ inject_into_file path, "#{indent}# Lighthouse's: the class of the SDL type of its name lends that type its methods.\n" \
126
+ "#{indent}include Lighthouse::GraphQL::Type\n", after: anchor
127
+ end
128
+ end
129
+
130
+ # graphql-ruby's controller, as its install wrote it, serves the schema with
131
+ # Lighthouse's concern. One the app has changed is left alone.
132
+ def adapt_controller
133
+ return if options[:skip_controller] || !exist?(CONTROLLER)
134
+ return if read(CONTROLLER).include?('Lighthouse::GraphQL::Controller')
135
+ return @kept_controller = true unless graphql_ruby_controller?(read(CONTROLLER))
64
136
 
65
- template 'graphql_controller.rb.tt', CONTROLLER
137
+ template 'graphql_controller.rb.tt', CONTROLLER, force: true
66
138
  end
67
139
 
68
140
  def add_route
@@ -72,8 +144,7 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
72
144
  return unless File.exist?(routes)
73
145
  return if File.read(routes).include?('graphql#execute')
74
146
 
75
- # Byte-identical to graphql-ruby's install route, so whichever generator
76
- # runs second finds it in place and does not add it again.
147
+ # Byte-identical to graphql-ruby's install route.
77
148
  route 'post "/graphql", to: "graphql#execute"'
78
149
  end
79
150
 
@@ -94,40 +165,49 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
94
165
  def show_next_steps
95
166
  serve =
96
167
  if @kept_controller
97
- "Serve it: #{CONTROLLER} was kept — have it call #{schema_name}.execute(...) to serve Lighthouse's schema."
168
+ "Serve it: #{CONTROLLER} was changed from graphql-ruby's, so it was kept — " \
169
+ "include Lighthouse::GraphQL::Controller in it, or keep calling #{schema_name}.execute(...)."
98
170
  else
99
171
  'Query it: POST /graphql {"query": "{ users { data { id name } paginatorInfo { total } } }"}'
100
172
  end
101
173
 
102
174
  say <<~MSG
103
175
 
104
- Lighthouse is installed. Next steps:
176
+ Lighthouse is installed on graphql-ruby's install (#{schema_path}). Next steps:
105
177
  1. Describe your API in #{sdl_folder}/*.graphql (the starter schema assumes a User model).
106
- 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}
178
+ 2. Resolve a field in Ruby when a directive is not enough, as in graphql-ruby: a method of
179
+ Types::QueryType (Query.latestPosts -> #latest_posts), a `field` of its own there,
180
+ `bin/rails g graphql:mutation`, or a Resolvers::BaseResolver subclass named after the field.
181
+ 3. Give a type Ruby methods, a union or interface its resolve_type, a scalar its coercion:
182
+ bin/rails g graphql:object Post (or graphql:union, graphql:interface, graphql:scalar);
183
+ the class is Lighthouse's through its base (Lighthouse::GraphQL::Type)
184
+ 4. Generate the IDE helper files: bin/rails lighthouse:ide_helper
185
+ 5. #{serve}
110
186
  MSG
111
187
  end
112
188
 
113
189
  private
114
190
 
115
- # Files another generator may own (graphql-ruby's install writes the same
116
- # controller) are left alone unless --force is given.
117
- def keep_existing?(path)
118
- return false if options[:force] || !File.exist?(File.join(destination_root, path))
119
-
120
- say_status :skip, "#{path} already exists (pass --force to replace it)", :yellow
121
- true
191
+ def schema_superclass(indent)
192
+ lines = base_types.map { |key, name| "#{indent} #{key}: #{name}" }
193
+ "Lighthouse::GraphQL::RbLightHouse.get_schema(\n#{indent} options: {\n#{indent} base_types: {\n" \
194
+ "#{lines.join(",\n")}\n#{indent} }\n#{indent} }\n#{indent})"
122
195
  end
123
196
 
124
- def schema_name
125
- options[:schema].camelize
197
+ def schema_comment(indent)
198
+ <<~RUBY.gsub(/^/, indent).delete_prefix(indent)
199
+ # Lighthouse builds the schema from the SDL in #{sdl_folder}/, on the classes graphql-ruby's
200
+ # install wrote (base_types): the SDL's root types extend the code-first ones, whose
201
+ # `field`s stay the schema's, and every type, resolver and mutation class inherits from
202
+ # its kind's base. The rest of the class is graphql-ruby's.
203
+ RUBY
126
204
  end
127
205
 
128
- # `Resolvers::BaseResolver`, for the schema class template.
129
- def base_class_constant(kind)
130
- "#{Lighthouse::Config.namespaces[kind].first}::#{base_class_name(kind)}"
206
+ def add_dataloader
207
+ anchor = /^([ \t]*)use GraphQL::Batch\n/
208
+ indent = read(schema_path)[anchor, 1]
209
+ inject_into_file schema_path, "#{indent}# Lighthouse's directives batch through GraphQL::Dataloader, beside GraphQL::Batch.\n" \
210
+ "#{indent}use GraphQL::Dataloader\n", after: anchor
131
211
  end
132
212
 
133
213
  def sdl_folder
@@ -17,21 +17,23 @@ 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 a
23
+ # subscription, builder or validator with `rails g lighthouse:subscription|builder|validator Name`,
24
+ # and a type class with graphql-ruby's `rails g graphql:object|union|interface|scalar Name`.
22
25
  # config.namespaces = {
23
26
  # queries: ['Resolvers'], mutations: ['Mutations'], subscriptions: ['Subscriptions'],
24
- # builders: ['Builders'], validators: ['Validators'], events: ['Events'], analyzers: ['Analyzers']
27
+ # builders: ['Builders'], validators: ['Validators'], events: ['Events'], analyzers: ['Analyzers'],
28
+ # types: ['Types']
25
29
  # }
26
30
 
27
31
  # 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.
32
+ # @field(resolver:) on types other than Query and Mutation, after :queries
33
+ # (@field(resolver: "Posts::TagsCount") is Resolvers::Posts::TagsCount).
34
+ # Fully-qualified names (containing `::`) are used as-is.
30
35
  # config.resolver_namespaces = ['App::Graphql']
31
36
 
32
- # Store for @cache and @clearCache.
33
- # config.cache_store = Rails.cache
34
-
35
37
  # Run @create/@update/@createMany/@updateMany in a database transaction.
36
38
  # config.transactional_mutations = true
37
39
 
@@ -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
@@ -1,11 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support/cache'
3
4
  require 'digest'
4
5
  require 'json'
5
6
  require 'securerandom'
6
7
 
7
- # Keys and invalidation for @cache / @clearCache over Config.cache_store
8
- # (Rails.cache by default).
8
+ # The app's cache stores, and keys and invalidation for @cache / @clearCache.
9
+ #
10
+ # Lighthouse keeps no store of its own, as PHP Lighthouse uses Laravel's: @cache,
11
+ # @clearCache and APQ use Rails.cache (config.cache_store), and @throttle counts
12
+ # where Rails' own `rate_limit` does, in the controller's cache_store.
9
13
  #
10
14
  # PHP Lighthouse invalidates with cache tags, which Rails cache stores do not
11
15
  # have (and `delete_matched` is not universal). Instead every entry key embeds
@@ -19,8 +23,10 @@ module Lighthouse::GraphQL::Caching
19
23
 
20
24
  module_function
21
25
 
26
+ # Rails.cache; outside Rails, one in-memory store for the process.
22
27
  def store
23
- Lighthouse::Config.cache_store
28
+ rails_cache = ::Rails.cache if defined?(::Rails) && ::Rails.respond_to?(:cache)
29
+ rails_cache || (@memory_store ||= ActiveSupport::Cache::MemoryStore.new)
24
30
  end
25
31
 
26
32
  def entry_key(type, id, field, args, user_key)
@@ -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
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_support/cache'
4
-
5
3
  module Lighthouse
6
4
  # Convenience entrypoint: `Lighthouse.configure { |c| c.resolver_namespaces = [...] }`.
7
5
  def self.configure(&block)
@@ -20,25 +18,27 @@ module Lighthouse
20
18
  validators: ['Validators'],
21
19
  events: ['Events'],
22
20
  subscriptions: ['Subscriptions'],
23
- analyzers: ['Analyzers']
21
+ analyzers: ['Analyzers'],
22
+ types: ['Types']
24
23
  }.freeze
25
24
 
26
25
  # Every setting reset! forgets, so the next read falls back to its default.
27
26
  SETTINGS = %i[
28
- sdl_folder resolver_namespaces namespaces cache_store transactional_mutations pagination_default_count pagination_max_count
27
+ sdl_folder resolver_namespaces namespaces transactional_mutations pagination_default_count pagination_max_count
29
28
  schema_cache schema_cache_path watch_sdl persisted_queries persisted_queries_store persisted_queries_ttl
30
29
  persisted_queries_manifest persisted_queries_only on_error directive_map authenticated_user policy_user authorizer
31
30
  feature_checker feature_actor
32
31
  convert_empty_strings_to_null
33
32
  soft_deletes
34
33
  queue_broadcasts broadcast_queue
34
+ global_id
35
35
  ].freeze
36
36
 
37
37
  class << self
38
- attr_writer :sdl_folder, :resolver_namespaces, :on_error, :authenticated_user, :policy_user, :authorizer, :cache_store,
38
+ attr_writer :sdl_folder, :resolver_namespaces, :on_error, :authenticated_user, :policy_user, :authorizer,
39
39
  :transactional_mutations, :schema_cache, :watch_sdl, :persisted_queries, :persisted_queries_store, :persisted_queries_only,
40
40
  :queue_broadcasts,
41
- :convert_empty_strings_to_null, :feature_checker, :feature_actor
41
+ :convert_empty_strings_to_null, :feature_checker, :feature_actor, :global_id
42
42
 
43
43
  def sdl_folder
44
44
  @sdl_folder ||= (defined?(Rails) && Rails.respond_to?(:root) ? Rails.root.join('graphql').to_s : nil)
@@ -64,16 +64,6 @@ module Lighthouse
64
64
  @namespaces = namespaces.merge(overrides.to_h.transform_keys(&:to_sym).transform_values { |v| Array(v) })
65
65
  end
66
66
 
67
- # Store backing @cache and @clearCache. Defaults to Rails.cache.
68
- def cache_store
69
- @cache_store ||=
70
- if defined?(Rails) && Rails.respond_to?(:cache) && Rails.cache
71
- Rails.cache
72
- else
73
- ActiveSupport::Cache::MemoryStore.new
74
- end
75
- end
76
-
77
67
  # --- Subscriptions (used by @broadcast) ----------------------------------
78
68
  #
79
69
  # queue_broadcasts perform @broadcast triggers in a Lighthouse::GraphQL::BroadcastJob
@@ -89,7 +79,7 @@ module Lighthouse
89
79
  # schema_cache_path where; default Rails.root/tmp/cache/lighthouse
90
80
  # watch_sdl reload the app (and so the schema) when a .graphql file changes
91
81
  # persisted_queries accept Apollo's Automatic Persisted Queries (PersistedQueries)
92
- # persisted_queries_store where APQ keeps queries; default cache_store
82
+ # persisted_queries_store where APQ keeps queries; default Rails.cache
93
83
  # persisted_queries_ttl how long they live there (seconds; nil: until evicted)
94
84
  # persisted_queries_manifest a manifest of trusted operations, served by hash
95
85
  # persisted_queries_only reject any operation not in the manifest
@@ -104,8 +94,12 @@ module Lighthouse
104
94
  # when the schema is built, so set it in an initializer.
105
95
  def convert_empty_strings_to_null = @convert_empty_strings_to_null || false
106
96
 
97
+ # Encodes and decodes Relay global ids (@globalId, @node): `encode(type, id)`
98
+ # and `decode(global_id)` -> [type, id]. Base64 of "Type:id" by default.
99
+ def global_id = @global_id || Lighthouse::GraphQL::GlobalId
100
+
107
101
  def persisted_queries_store
108
- @persisted_queries_store || cache_store
102
+ @persisted_queries_store || Lighthouse::GraphQL::Caching.store
109
103
  end
110
104
 
111
105
  # Pagination defaults, overridden by a directive's defaultCount / maxCount.
@@ -19,10 +19,16 @@ 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
  #
27
+ # A field's @throttle that renders past its limit (its `with:` method, or
28
+ # `head :too_many_requests`, rate_limit's default on Rails 7.2) ends the request
29
+ # with that response, as a before_action that renders does: the operation stops
30
+ # there (Performed), and `execute` renders nothing more.
31
+ #
26
32
  # Hooks, private so they are not actions: `graphql_context` (default:
27
33
  # `{ current_user: current_user }` when the controller has a current_user, else
28
34
  # `{}`), `graphql_schema` and `graphql_variables(param)`. A controller with an
@@ -31,6 +37,11 @@ require 'json'
31
37
  module Lighthouse::GraphQL::Controller
32
38
  extend ActiveSupport::Concern
33
39
 
40
+ # Raised once a @throttle `with:` method has performed the response: control
41
+ # flow, as `throw :abort` in a before_action, so no `rescue StandardError` (a
42
+ # schema's rescue_from) catches it on its way out of the operation.
43
+ class Performed < Exception; end # rubocop:disable Lint/InheritException
44
+
34
45
  included do
35
46
  class_attribute :lighthouse_schema_source, instance_writer: false
36
47
  end
@@ -46,17 +57,17 @@ module Lighthouse::GraphQL::Controller
46
57
 
47
58
  def execute
48
59
  lighthouse_request do
49
- batch = params[:_json]
50
- if batch.is_a?(Array)
51
- render json: lighthouse_batch(batch)
60
+ operations = lighthouse_operations
61
+ if operations.is_a?(Array)
62
+ render json: lighthouse_batch(operations.map { |operation| lighthouse_hash(operation) })
52
63
  else
53
- result = lighthouse_operation
64
+ result = lighthouse_operation(operations)
54
65
  Lighthouse::GraphQL::CacheControl.apply(response, result)
55
66
  render json: result
56
67
  end
57
68
  end
58
69
  rescue StandardError => e
59
- raise e unless defined?(::Rails) && ::Rails.env.development?
70
+ raise e unless defined?(::Rails) && ::Rails.env.development? && !rails_response?(e)
60
71
 
61
72
  lighthouse_development_error(e)
62
73
  end
@@ -64,13 +75,18 @@ module Lighthouse::GraphQL::Controller
64
75
  private
65
76
 
66
77
  # Dispatch StartRequest, run the block, dispatch EndRequest with the
67
- # response. Only the outermost call dispatches.
78
+ # response. Only the outermost call dispatches, and stops the block where a
79
+ # @throttle `with:` method performed the response.
68
80
  def lighthouse_request
69
81
  return yield if @lighthouse_request_started
70
82
 
71
83
  @lighthouse_request_started = true
72
84
  Lighthouse::Events.dispatch(Lighthouse::Events::StartRequest.new(request))
73
- result = yield
85
+ result = begin
86
+ yield
87
+ rescue Performed
88
+ nil
89
+ end
74
90
  Lighthouse::Events.dispatch(Lighthouse::Events::EndRequest.new(response))
75
91
  result
76
92
  end
@@ -80,6 +96,13 @@ module Lighthouse::GraphQL::Controller
80
96
  respond_to?(:current_user, true) ? { current_user: current_user } : {}
81
97
  end
82
98
 
99
+ # graphql_context, with what @throttle reads unless it sets its own: the
100
+ # client's IP (rate_limit's default by:) and the controller, whose cache_store,
101
+ # controller_path and methods rate_limit uses.
102
+ def lighthouse_context
103
+ { remote_ip: request.remote_ip, lighthouse_controller: self }.merge(graphql_context)
104
+ end
105
+
83
106
  def graphql_schema
84
107
  source = lighthouse_schema_source
85
108
  case source
@@ -103,11 +126,23 @@ module Lighthouse::GraphQL::Controller
103
126
  end
104
127
  end
105
128
 
106
- def lighthouse_operation
129
+ # A multipart request's operation(s), a batch (a JSON array body), or the parameters.
130
+ def lighthouse_operations
131
+ Lighthouse::GraphQL::MultipartRequest.operations(params) || params[:_json] || params
132
+ end
133
+
134
+ # Parameters, or a Hash from a multipart request's JSON, read by key either way.
135
+ def lighthouse_hash(operation)
136
+ (operation.respond_to?(:to_unsafe_h) ? operation.to_unsafe_h : operation.to_h).with_indifferent_access
137
+ end
138
+
139
+ # `operation`: the request's parameters, or a multipart request's operation.
140
+ def lighthouse_operation(operation = params)
141
+ operation = operation.with_indifferent_access if operation.is_a?(Hash)
107
142
  Lighthouse::GraphQL::PersistedQueries.execute(
108
143
  graphql_schema,
109
- query: params[:query], extensions: params[:extensions],
110
- variables: graphql_variables(params[:variables]), operation_name: params[:operationName], context: graphql_context
144
+ query: operation[:query], extensions: operation[:extensions],
145
+ variables: graphql_variables(operation[:variables]), operation_name: operation[:operationName], context: lighthouse_context
111
146
  )
112
147
  end
113
148
 
@@ -116,11 +151,17 @@ module Lighthouse::GraphQL::Controller
116
151
  def lighthouse_batch(operations)
117
152
  operations = operations.map do |operation|
118
153
  { query: operation[:query], extensions: operation[:extensions], variables: graphql_variables(operation[:variables]),
119
- operation_name: operation[:operationName], context: graphql_context }
154
+ operation_name: operation[:operationName], context: lighthouse_context }
120
155
  end
121
156
  Lighthouse::GraphQL::PersistedQueries.multiplex(graphql_schema, operations)
122
157
  end
123
158
 
159
+ # An error Rails answers with a status of its own (config.action_dispatch.rescue_responses):
160
+ # ActionController::TooManyRequests from @throttle is a 429 in development too.
161
+ def rails_response?(error)
162
+ ::ActionDispatch::ExceptionWrapper.status_code_for_exception(error.class.name) != 500
163
+ end
164
+
124
165
  def lighthouse_development_error(error)
125
166
  logger.error(error.message)
126
167
  logger.error(error.backtrace.join("\n"))
@@ -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