lighthouse-graphql 0.6.0 → 0.7.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +95 -1
  3. data/README.md +15 -7
  4. data/docs/best-practices.md +6 -4
  5. data/docs/configuration.md +19 -5
  6. data/docs/custom-directives.md +2 -1
  7. data/docs/directives.md +65 -7
  8. data/docs/federation.md +1 -1
  9. data/docs/filtering-and-ordering.md +330 -30
  10. data/docs/generators.md +1 -1
  11. data/docs/getting-started.md +43 -10
  12. data/docs/motivation.md +3 -2
  13. data/docs/performance.md +4 -3
  14. data/docs/relationships.md +2 -1
  15. data/docs/resolvers.md +2 -2
  16. data/docs/schema-organisation.md +177 -0
  17. data/lib/generators/lighthouse/install/install_generator.rb +16 -11
  18. data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +2 -1
  19. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +11 -4
  20. data/lib/lighthouse/ast_builder.rb +124 -0
  21. data/lib/lighthouse/config.rb +23 -4
  22. data/lib/lighthouse/contracts/arg_manipulator.rb +9 -5
  23. data/lib/lighthouse/contracts/field_builder.rb +13 -0
  24. data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +1 -2
  25. data/lib/lighthouse/directive_resolvers/all_resolver.rb +0 -2
  26. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +9 -6
  27. data/lib/lighthouse/directive_resolvers/base.rb +6 -37
  28. data/lib/lighthouse/directive_resolvers/builder_applier.rb +21 -6
  29. data/lib/lighthouse/directive_resolvers/count_resolver.rb +0 -1
  30. data/lib/lighthouse/directive_resolvers/first_resolver.rb +0 -2
  31. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +2 -3
  32. data/lib/lighthouse/directives/arguments/order_by.rb +190 -26
  33. data/lib/lighthouse/directives/arguments/where_conditions.rb +25 -0
  34. data/lib/lighthouse/directives/arguments/where_conditions_base.rb +84 -0
  35. data/lib/lighthouse/directives/arguments/where_has_conditions.rb +56 -0
  36. data/lib/lighthouse/directives/definitions.rb +0 -1
  37. data/lib/lighthouse/directives/generated_types.rb +103 -0
  38. data/lib/lighthouse/directives/registry.rb +39 -10
  39. data/lib/lighthouse/document_ast.rb +111 -0
  40. data/lib/lighthouse/graphql/version.rb +1 -1
  41. data/lib/lighthouse/ide_helper.rb +80 -23
  42. data/lib/lighthouse/namespaces.rb +1 -0
  43. data/lib/lighthouse/order_by/types.rb +137 -0
  44. data/lib/lighthouse/railtie.rb +9 -2
  45. data/lib/lighthouse/rb_lighthouse.rb +15 -5
  46. data/lib/lighthouse/resolve_info.rb +5 -5
  47. data/lib/lighthouse/schema_cache.rb +8 -4
  48. data/lib/lighthouse/schema_factory.rb +6 -9
  49. data/lib/lighthouse/schema_generator.rb +4 -15
  50. data/lib/lighthouse/schema_stitcher.rb +64 -0
  51. data/lib/lighthouse/support/naming.rb +0 -12
  52. data/lib/lighthouse/where_conditions/columns.rb +36 -0
  53. data/lib/lighthouse/where_conditions/handler.rb +152 -0
  54. data/lib/lighthouse/where_conditions/json_column.rb +99 -0
  55. data/lib/lighthouse/where_conditions/operator_map.rb +1 -4
  56. data/lib/lighthouse/where_conditions/types.rb +110 -0
  57. data/sig/lighthouse/directives.rbs +16 -3
  58. data/sig/lighthouse/graphql.rbs +48 -4
  59. data/sig/lighthouse.rbs +8 -2
  60. metadata +15 -4
  61. data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +0 -170
  62. data/lib/lighthouse/directives/where_conditions_directive.rb +0 -244
  63. data/lib/lighthouse/sdl_loader.rb +0 -22
@@ -0,0 +1,177 @@
1
+ # Schema Organisation
2
+
3
+ Lighthouse reads your schema from a single entrypoint, `graphql/schema.graphql`
4
+ by default (`config.schema_path`), as PHP Lighthouse does. As the schema grows,
5
+ you split it across files, and the entrypoint imports them.
6
+
7
+ ## Schema imports
8
+
9
+ ```
10
+ graphql/
11
+ |-- schema.graphql
12
+ |-- user.graphql
13
+ ```
14
+
15
+ An import starts with `#import`, followed by the path of the imported file,
16
+ relative to the file the import is in:
17
+
18
+ ```graphql
19
+ type Query {
20
+ user: User
21
+ }
22
+
23
+ #import user.graphql
24
+ ```
25
+
26
+ The contents of `user.graphql` are pasted in where the import is:
27
+
28
+ ```graphql
29
+ type Query {
30
+ user: User
31
+ }
32
+
33
+ type User {
34
+ name: String!
35
+ }
36
+ ```
37
+
38
+ An imported file can import others. A file is read once, so importing it twice,
39
+ or importing a file that imports it back, adds nothing more.
40
+
41
+ Only the entrypoint and the files it imports make up the schema. A `.graphql`
42
+ file in the folder that nothing imports is not part of it. A missing import
43
+ fails the build: "Did not find GraphQL schema import at …".
44
+
45
+ ### Wildcard imports
46
+
47
+ ```
48
+ graphql/
49
+ |-- schema.graphql
50
+ |-- blog/
51
+ |-- post.graphql
52
+ |-- category.graphql
53
+ ```
54
+
55
+ To import every file in `blog/` at once, use a glob (Ruby's `Dir.glob`), which
56
+ imports the files in name order:
57
+
58
+ ```graphql
59
+ #import blog/*.graphql
60
+ ```
61
+
62
+ `#import **/*.graphql` imports every file below the entrypoint's folder.
63
+
64
+ ## Type extensions
65
+
66
+ A type is defined once. Defining it twice is an error that points at
67
+ `extend`. This applies even across files:
68
+
69
+ ```
70
+ Duplicate definition Query found when parsing the schema. Define a type once and add to it from other files with `extend type Query`.
71
+ ```
72
+
73
+ PHP Lighthouse keeps the last definition and drops the earlier one without a
74
+ word. Lighthouse here makes that an error, so a type's fields are never lost.
75
+
76
+ Say `Post` is defined in `post.graphql`:
77
+
78
+ ```graphql
79
+ type Post {
80
+ title: String
81
+ author: User @belongsTo
82
+ }
83
+ ```
84
+
85
+ and `schema.graphql` imports it:
86
+
87
+ ```graphql
88
+ #import post.graphql
89
+ ```
90
+
91
+ To add the queries that fetch posts, you could add them to the `Query` type in
92
+ `schema.graphql`. It is usually better to keep queries next to the type they
93
+ return. Make sure `schema.graphql` defines `Query`, even with no fields:
94
+
95
+ ```graphql
96
+ type Query
97
+ ```
98
+
99
+ Then extend `Query` in `post.graphql`:
100
+
101
+ ```graphql
102
+ type Post {
103
+ title: String
104
+ author: User @belongsTo
105
+ }
106
+
107
+ extend type Query {
108
+ posts: [Post!]! @paginate
109
+ }
110
+ ```
111
+
112
+ The fields of the `extend type` are merged into the type's definition, and the
113
+ directives on them work as on any field. Besides object types, you can extend
114
+ `input`, `interface`, `enum`, `union` and `scalar` types:
115
+
116
+ ```graphql
117
+ extend input PostFilter { authorId: ID @eq(key: "author_id") }
118
+ extend enum Role { EDITOR }
119
+ extend union SearchResult = Comment
120
+ ```
121
+
122
+ The final schema always has one type per name. An extension of a type that is
123
+ not defined is an error, and so is an extension that redefines a field:
124
+
125
+ ```
126
+ Could not find a base definition User of kind ObjectTypeExtension to extend.
127
+ Duplicate definition title found when merging.
128
+ ```
129
+
130
+ The root types `Query`, `Mutation` and `Subscription` are the exception: an
131
+ `extend type Mutation` with no `type Mutation` anywhere defines it.
132
+
133
+ ## How the schema is built
134
+
135
+ The build runs these phases in order, as PHP Lighthouse's `ASTBuilder` does:
136
+
137
+ 1. The entrypoint and its imports are read (`Lighthouse::GraphQL::SchemaStitcher`),
138
+ along with what `BuildSchemaString` listeners add
139
+ ([events.md](events.md)).
140
+ 2. Every type is defined once (`Lighthouse::GraphQL::DocumentAST`).
141
+ 3. Type extensions are merged into their types.
142
+ 4. Type and field manipulators run: `@hide`/`@show`, `@model`, `@paginate`
143
+ and the relation directives' `type:`, `@softDeletes`, `@node`.
144
+ 5. Argument manipulators run: `@whereConditions`, `@whereHasConditions` and
145
+ `@orderBy` replace the `_` placeholder with the types they generate
146
+ ([filtering-and-ordering.md](filtering-and-ordering.md)). An argument still
147
+ typed `_` after this phase is an error.
148
+ 6. `ManipulateAST` listeners see the document.
149
+
150
+ ## IDE support
151
+
152
+ An IDE reads the `.graphql` files as they are written, so it does not know
153
+ Lighthouse's directives, the types the build generates, or the `_` placeholder.
154
+ Lighthouse writes two files that declare them:
155
+
156
+ - `schema-directives.graphql`: every directive the schema uses and does not declare.
157
+ - `programmatic-types.graphql`: the generated types (paginators, the `@orderBy`
158
+ and `@whereConditions` inputs, ...) and `scalar _`.
159
+
160
+ `graphql.config.yml` (written by `lighthouse:install`) points the GraphQL
161
+ plugins of VS Code and JetBrains IDEs at them, next to every `.graphql` file:
162
+
163
+ ```yaml
164
+ schema:
165
+ - graphql/**/*.graphql
166
+ - schema-directives.graphql
167
+ - programmatic-types.graphql
168
+ ```
169
+
170
+ In development, Lighthouse rewrites both files on boot and on every reload when
171
+ the app has a `graphql.config.yml` (`config.ide_helper`). Elsewhere, run
172
+ `bin/rails lighthouse:ide_helper`. No type is in two of the three sources, because
173
+ graphql-js, which the IDE plugins build the schema with, rejects a type defined
174
+ twice.
175
+
176
+ The IDE sees the files as one schema, imports or not. Without the generated
177
+ files it reports `Unknown type "_"` and unknown directives.
@@ -44,8 +44,8 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
44
44
 
45
45
  class_option :schema, type: :string,
46
46
  desc: "The schema class to adopt; for graphql:install, when it runs, its name (default: graphql-ruby's)"
47
- class_option :sdl_folder, type: :string, default: 'graphql',
48
- desc: 'Folder, relative to the app root, holding the *.graphql files'
47
+ class_option :schema_path, type: :string, default: 'graphql/schema.graphql',
48
+ desc: "The schema's entrypoint, relative to the app root; it #imports the other .graphql files"
49
49
  class_option :subscriptions, type: :boolean, default: false,
50
50
  desc: 'Add Subscriptions::BaseSubscription, the base of the classes Subscription fields resolve to'
51
51
  class_option :skip_controller, type: :boolean, default: false,
@@ -71,12 +71,12 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
71
71
  end
72
72
 
73
73
  def create_starter_schema
74
- if Dir[File.join(destination_root, sdl_folder, '**', '*.graphql')].any?
75
- say_status :skip, "#{sdl_folder}/ already has .graphql files", :yellow
74
+ if File.exist?(File.join(destination_root, sdl_schema_path))
75
+ say_status :skip, "#{sdl_schema_path} exists", :yellow
76
76
  return
77
77
  end
78
78
 
79
- template 'schema.graphql.tt', File.join(sdl_folder, 'schema.graphql')
79
+ template 'schema.graphql.tt', sdl_schema_path
80
80
  end
81
81
 
82
82
  # Subscriptions::BaseSubscription, which graphql-ruby has none of (see BaseClasses).
@@ -174,14 +174,14 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
174
174
  say <<~MSG
175
175
 
176
176
  Lighthouse is installed on graphql-ruby's install (#{schema_path}). Next steps:
177
- 1. Describe your API in #{sdl_folder}/*.graphql (the starter schema assumes a User model).
177
+ 1. Describe your API in #{sdl_schema_path} (the starter assumes a User model); #import more files, which `extend type Query`.
178
178
  2. Resolve a field in Ruby when a directive is not enough, as in graphql-ruby: a method of
179
179
  Types::QueryType (Query.latestPosts -> #latest_posts), a `field` of its own there,
180
180
  `bin/rails g graphql:mutation`, or a Resolvers::BaseResolver subclass named after the field.
181
181
  3. Give a type Ruby methods, a union or interface its resolve_type, a scalar its coercion:
182
182
  bin/rails g graphql:object Post (or graphql:union, graphql:interface, graphql:scalar);
183
183
  the class is Lighthouse's through its base (Lighthouse::GraphQL::Type)
184
- 4. Generate the IDE helper files: bin/rails lighthouse:ide_helper
184
+ 4. The IDE helper files are rewritten in development; or: bin/rails lighthouse:ide_helper
185
185
  5. #{serve}
186
186
  MSG
187
187
  end
@@ -196,7 +196,7 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
196
196
 
197
197
  def schema_comment(indent)
198
198
  <<~RUBY.gsub(/^/, indent).delete_prefix(indent)
199
- # Lighthouse builds the schema from the SDL in #{sdl_folder}/, on the classes graphql-ruby's
199
+ # Lighthouse builds the schema from the SDL in #{sdl_schema_path}, on the classes graphql-ruby's
200
200
  # install wrote (base_types): the SDL's root types extend the code-first ones, whose
201
201
  # `field`s stay the schema's, and every type, resolver and mutation class inherits from
202
202
  # its kind's base. The rest of the class is graphql-ruby's.
@@ -210,12 +210,17 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
210
210
  "#{indent}use GraphQL::Dataloader\n", after: anchor
211
211
  end
212
212
 
213
+ # The SDL's entrypoint (Config.schema_path); `schema_path` is graphql-ruby's schema class.
214
+ def sdl_schema_path
215
+ options[:schema_path].delete_prefix('/')
216
+ end
217
+
213
218
  def sdl_folder
214
- options[:sdl_folder].delete_suffix('/')
219
+ File.dirname(sdl_schema_path)
215
220
  end
216
221
 
217
- def default_sdl_folder?
218
- sdl_folder == 'graphql'
222
+ def default_sdl_schema_path?
223
+ sdl_schema_path == 'graphql/schema.graphql'
219
224
  end
220
225
 
221
226
  def ide_helper_files
@@ -1,6 +1,7 @@
1
1
  # graphql-config, read by the GraphQL plugins for JetBrains IDEs and VS Code.
2
2
  # The two generated files describe Lighthouse's directives and the types it
3
- # generates; refresh them after changing the schema:
3
+ # generates. In development they are rewritten on boot and on every reload
4
+ # (config.ide_helper); elsewhere, refresh them with:
4
5
  # bin/rails lighthouse:ide_helper
5
6
  schema:
6
7
  - <%= sdl_folder %>/**/*.graphql
@@ -3,11 +3,13 @@
3
3
  # Lighthouse configuration. Every option is optional; the defaults are shown.
4
4
  # Reference: https://github.com/Vaz-Innovation/lighthouse-graphql/blob/main/docs/configuration.md
5
5
  Lighthouse.configure do |config|
6
- # Folder the *.graphql schema files are loaded from.
7
- <% if default_sdl_folder? -%>
8
- # config.sdl_folder = Rails.root.join('graphql').to_s
6
+ # The schema's entrypoint. It imports the other .graphql files
7
+ # (#import user.graphql, #import blog/*.graphql), which add to a type
8
+ # defined elsewhere with `extend type`.
9
+ <% if default_sdl_schema_path? -%>
10
+ # config.schema_path = Rails.root.join('graphql/schema.graphql').to_s
9
11
  <% else -%>
10
- config.sdl_folder = Rails.root.join('<%= sdl_folder %>').to_s
12
+ config.schema_path = Rails.root.join('<%= sdl_schema_path %>').to_s
11
13
  <% end -%>
12
14
 
13
15
  # Where classes named by the schema are looked up, by kind. A Query or Mutation
@@ -58,6 +60,11 @@ Lighthouse.configure do |config|
58
60
  # Reload the app (and so the schema) when a .graphql file changes.
59
61
  # config.watch_sdl = Rails.env.development?
60
62
 
63
+ # Rewrite schema-directives.graphql and programmatic-types.graphql, which your
64
+ # IDE reads through graphql.config.yml, on boot and on every reload. nil: in
65
+ # development, when the app has a graphql.config.yml.
66
+ # config.ide_helper = nil
67
+
61
68
  # Automatic Persisted Queries (Apollo's protocol). Add a GET route too, so
62
69
  # hashed queries can be cached by CDNs: get "/graphql", to: "graphql#execute"
63
70
  # config.persisted_queries = true
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The manipulation phase, as PHP Lighthouse's ASTBuilder runs it on the
4
+ # stitched schema:
5
+ #
6
+ # 1. DocumentAST.from_source: every type defined once.
7
+ # 2. Type extensions merged into the types they extend. An extension of a
8
+ # root type that has no definition (`extend type Query` alone) defines it.
9
+ # 3. The type and field manipulators: @hide/@show, @model, @paginate (and the
10
+ # relation directives' `type:`), @softDeletes, @node.
11
+ # 4. The argument manipulators: each argument directive whose class includes
12
+ # Contracts::ArgManipulator rewrites its argument (@whereConditions,
13
+ # @whereHasConditions, @orderBy turn `_` into the input types they generate).
14
+ # 5. The definitions of directives the generated types use (`@enum`).
15
+ #
16
+ # Returns the manipulated SDL; ManipulateAST listeners then see its document.
17
+ module Lighthouse::GraphQL::ASTBuilder
18
+ Nodes = ::GraphQL::Language::Nodes
19
+
20
+ EXTENSION_TO_DEFINITION = {
21
+ Nodes::ObjectTypeExtension => Nodes::ObjectTypeDefinition,
22
+ Nodes::InterfaceTypeExtension => Nodes::InterfaceTypeDefinition,
23
+ Nodes::InputObjectTypeExtension => Nodes::InputObjectTypeDefinition,
24
+ Nodes::EnumTypeExtension => Nodes::EnumTypeDefinition,
25
+ Nodes::UnionTypeExtension => Nodes::UnionTypeDefinition,
26
+ Nodes::ScalarTypeExtension => Nodes::ScalarTypeDefinition
27
+ }.freeze
28
+
29
+ # What each extension adds to its definition, besides directives.
30
+ MERGED_LISTS = {
31
+ Nodes::ObjectTypeExtension => %i[fields interfaces],
32
+ Nodes::InterfaceTypeExtension => %i[fields interfaces],
33
+ Nodes::InputObjectTypeExtension => %i[fields],
34
+ Nodes::EnumTypeExtension => %i[values],
35
+ Nodes::UnionTypeExtension => %i[types],
36
+ Nodes::ScalarTypeExtension => []
37
+ }.freeze
38
+
39
+ ROOT_TYPES = %w[Query Mutation Subscription].freeze
40
+
41
+ OBJECT_LIKE = [Nodes::ObjectTypeDefinition, Nodes::InterfaceTypeDefinition].freeze
42
+
43
+ module_function
44
+
45
+ def build(sdl, registry: Lighthouse::DirectiveRegistry.default)
46
+ document_ast = Lighthouse::GraphQL::DocumentAST.from_source(sdl)
47
+ extend_types(document_ast)
48
+
49
+ document_ast = Lighthouse::GraphQL::DocumentAST.from_source(registry.manipulate_sdl(document_ast.to_sdl))
50
+ apply_arg_manipulators(document_ast, registry)
51
+
52
+ Lighthouse::GraphQL::SchemaGenerator.inject_directive_definitions(document_ast.to_sdl)
53
+ end
54
+
55
+ def extend_types(document_ast)
56
+ document_ast.type_extensions.each do |name, extensions|
57
+ extensions.each { |extension| extend_type(document_ast, name, extension) }
58
+ end
59
+ document_ast.type_extensions.clear
60
+ end
61
+
62
+ def extend_type(document_ast, name, extension)
63
+ definition = document_ast.types[name] || root_type(document_ast, name, extension)
64
+ unless definition.is_a?(EXTENSION_TO_DEFINITION.fetch(extension.class))
65
+ raise Lighthouse::DefinitionError,
66
+ "The type extension #{name} of kind #{kind(extension)} can not extend a definition of kind #{kind(definition)}."
67
+ end
68
+
69
+ merged = MERGED_LISTS.fetch(extension.class).index_with { |list| merge_unique(definition.public_send(list), extension.public_send(list)) }
70
+ document_ast.put_type(definition.merge(directives: definition.directives + extension.directives, **merged))
71
+ end
72
+
73
+ # `extend type Query` with no `type Query` anywhere defines it, as in PHP Lighthouse.
74
+ def root_type(document_ast, name, extension)
75
+ unless ROOT_TYPES.include?(name) && extension.is_a?(Nodes::ObjectTypeExtension)
76
+ raise Lighthouse::DefinitionError, "Could not find a base definition #{name} of kind #{kind(extension)} to extend."
77
+ end
78
+
79
+ Nodes::ObjectTypeDefinition.new(name: name, fields: [], interfaces: [], directives: []).tap { |root| document_ast.put_type(root) }
80
+ end
81
+
82
+ # The definition's nodes followed by the extension's; a name in both is an error.
83
+ def merge_unique(original, addition)
84
+ names = addition.to_set(&:name)
85
+ duplicate = original.find { |node| names.include?(node.name) }
86
+ raise Lighthouse::DefinitionError, "Duplicate definition #{duplicate.name} found when merging." if duplicate
87
+
88
+ original + addition
89
+ end
90
+
91
+ def kind(node)
92
+ node.class.name.demodulize
93
+ end
94
+
95
+ # Every argument of every object and interface field, through the
96
+ # manipulators of the directives it carries, in their SDL order.
97
+ def apply_arg_manipulators(document_ast, registry)
98
+ document_ast.type_names.each do |name|
99
+ type = document_ast.types[name]
100
+ next unless OBJECT_LIKE.any? { |kind| type.is_a?(kind) }
101
+
102
+ fields = type.fields.map do |field|
103
+ field.merge(arguments: field.arguments.map { |arg| manipulate_argument(document_ast, registry, arg, field, type) })
104
+ end
105
+ document_ast.put_type(type.merge(fields: fields))
106
+ end
107
+ end
108
+
109
+ def manipulate_argument(document_ast, registry, arg, field, type)
110
+ manipulated = arg.directives.reduce(arg) do |current, directive|
111
+ manipulator = registry.arg_manipulator_class(directive.name)
112
+ manipulator ? manipulator.new(current, directive).manipulate_arg_definition(document_ast, field, type) : current
113
+ end
114
+ return manipulated unless placeholder?(manipulated.type)
115
+
116
+ raise Lighthouse::DefinitionError,
117
+ "#{type.name}.#{field.name}(#{arg.name}:) has the placeholder type `_`, which only a directive that generates " \
118
+ 'its type replaces: @whereConditions, @whereHasConditions or @orderBy.'
119
+ end
120
+
121
+ def placeholder?(type)
122
+ Lighthouse::GraphQL::Directives::GeneratedTypes.placeholder?(type)
123
+ end
124
+ end
@@ -24,7 +24,7 @@ module Lighthouse
24
24
 
25
25
  # Every setting reset! forgets, so the next read falls back to its default.
26
26
  SETTINGS = %i[
27
- sdl_folder resolver_namespaces namespaces transactional_mutations pagination_default_count pagination_max_count
27
+ schema_path ide_helper resolver_namespaces namespaces transactional_mutations pagination_default_count pagination_max_count
28
28
  schema_cache schema_cache_path watch_sdl persisted_queries persisted_queries_store persisted_queries_ttl
29
29
  persisted_queries_manifest persisted_queries_only on_error directive_map authenticated_user policy_user authorizer
30
30
  feature_checker feature_actor
@@ -35,13 +35,32 @@ module Lighthouse
35
35
  ].freeze
36
36
 
37
37
  class << self
38
- attr_writer :sdl_folder, :resolver_namespaces, :on_error, :authenticated_user, :policy_user, :authorizer,
38
+ attr_writer :schema_path, :ide_helper, :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
41
  :convert_empty_strings_to_null, :feature_checker, :feature_actor, :global_id
42
42
 
43
- def sdl_folder
44
- @sdl_folder ||= (defined?(Rails) && Rails.respond_to?(:root) ? Rails.root.join('graphql').to_s : nil)
43
+ # The schema's entrypoint, as PHP Lighthouse's `schema_path`: the one file
44
+ # the schema is read from, importing the others (`#import user.graphql`,
45
+ # `#import blog/*.graphql`; see SchemaStitcher). RAILS_ROOT/graphql/schema.graphql
46
+ # by default.
47
+ def schema_path
48
+ @schema_path ||= (::Rails.root.join('graphql/schema.graphql').to_s if defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root)
49
+ end
50
+
51
+ # The folder of the schema's entrypoint, which watch_sdl watches.
52
+ def schema_directory
53
+ schema_path && File.dirname(schema_path.to_s)
54
+ end
55
+
56
+ # Rewrite the IDE helper files (schema-directives.graphql,
57
+ # programmatic-types.graphql) on boot and on every reload. nil (the
58
+ # default): in development, when the app has a graphql.config.yml.
59
+ def ide_helper
60
+ return @ide_helper unless @ide_helper.nil?
61
+
62
+ defined?(::Rails) && ::Rails.respond_to?(:env) && ::Rails.env.development? && ::Rails.root &&
63
+ File.exist?(::Rails.root.join('graphql.config.yml'))
45
64
  end
46
65
 
47
66
  def directive_map = @directive_map ||= {}
@@ -2,12 +2,16 @@
2
2
 
3
3
  require_relative 'directive'
4
4
 
5
- # A directive on a field ARGUMENT that mutates the schema AST before build,
6
- # e.g. @whereConditions / @whereHasConditions generating input/enum types and
7
- # swapping the `_` placeholder argument type.
5
+ # A directive on a field ARGUMENT that rewrites the schema before it is built,
6
+ # as PHP Lighthouse's ArgManipulator: @whereConditions, @whereHasConditions and
7
+ # @orderBy generate the input types of an argument written with the `_`
8
+ # placeholder, and type it with them.
8
9
  #
9
- # Implementors provide (at the class level)
10
- # `manipulate_argument_definition(manipulation_context, parent, field, arg, directive)`.
10
+ # Implementors, built with (arg_node, directive_node), provide
11
+ # `#manipulate_arg_definition(document_ast, field_node, type_node)` returning
12
+ # the argument node (graphql-ruby's nodes are immutable: `arg.merge(type: ...)`).
13
+ # New types go in the Lighthouse::GraphQL::DocumentAST (`define_type`).
14
+ # ASTBuilder calls it for every argument of an object or interface field.
11
15
  module Lighthouse::Contracts::ArgManipulator
12
16
  include Lighthouse::Contracts::Directive
13
17
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'directive'
4
+
5
+ # A directive on a FIELD that constrains the query of the field's resolver
6
+ # whatever the client sends, e.g. @orderBy(column:, direction:) on a field.
7
+ # Applied after the argument builders, as PHP Lighthouse's FieldBuilderDirective.
8
+ #
9
+ # Implementors, built with (field_node, directive_node), provide
10
+ # `#handle_field_builder(relation)` returning a relation.
11
+ module Lighthouse::Contracts::FieldBuilder
12
+ include Lighthouse::Contracts::Directive
13
+ end
@@ -25,13 +25,12 @@ class Lighthouse::GraphQL::DirectiveResolvers::AggregateDirectiveResolver < Ligh
25
25
  column_name = extract_directive_arg('column')
26
26
  function_name = extract_directive_arg('function')
27
27
  resolver_instance = self
28
- field_ast_node = @field_def.ast_node
29
28
 
30
29
  install(target_type_class, lambda do |obj, args, ctx|
31
30
  relation = resolver_instance.send(:aggregate_source, obj, model_name, relation_name)
32
31
  return nil unless relation
33
32
 
34
- relation = Lighthouse::GraphQL::DirectiveResolvers::BuilderApplier.apply(relation, field_ast_node, args, ctx, resolver_instance)
33
+ relation = resolver_instance.apply_where_filters(relation, args, ctx)
35
34
  resolver_instance.send(:compute_aggregation, relation, column_name, function_name)
36
35
  end)
37
36
  end
@@ -25,8 +25,6 @@ class Lighthouse::GraphQL::DirectiveResolvers::AllDirectiveResolver < Lighthouse
25
25
  args = arguments.respond_to?(:to_h) ? arguments.to_h : {}
26
26
  relation = apply_scopes(model_class.all)
27
27
 
28
- relation = Lighthouse::GraphQL::DirectiveResolvers::BuilderApplier.apply(relation, @field_def.ast_node, args, ctx, self)
29
-
30
28
  relation = apply_where_filters(relation, args, ctx)
31
29
  relation = relation.limit(args[:first]) if args[:first]
32
30
  relation = relation.offset(args[:offset]) if args[:offset]
@@ -36,7 +36,7 @@ module Lighthouse::GraphQL::DirectiveResolvers
36
36
  return nil unless obj.respond_to?(name)
37
37
 
38
38
  return paginate(obj, name, args, ctx) if pagination_type
39
- return constrained(association_scope(obj, name), args) if constrained?(args)
39
+ return constrained(association_scope(obj, name), args, ctx) if constrained?(args)
40
40
 
41
41
  dataloader = Lighthouse::Support::Batching.dataloader(ctx)
42
42
  return obj.public_send(name) unless dataloader && obj.class.respond_to?(:reflect_on_association)
@@ -52,11 +52,14 @@ module Lighthouse::GraphQL::DirectiveResolvers
52
52
  self.class.collection? && directive_arg('type')&.to_s&.upcase
53
53
  end
54
54
 
55
- # Scopes or a supplied argument with a filter directive need a query of
56
- # their own instead of the batched load.
55
+ # Scopes, a query directive on the field (@builder, @orderBy(column:)) or a
56
+ # supplied argument with a filter directive need a query of their own
57
+ # instead of the batched load.
57
58
  def constrained?(args)
58
- return true if directive_arg('scopes')
59
+ directive_arg('scopes') || Lighthouse::GraphQL::DirectiveResolvers::BuilderApplier.any?(@field_def.ast_node) || filtered?(args)
60
+ end
59
61
 
62
+ def filtered?(args)
60
63
  registry = Lighthouse::DirectiveRegistry.default
61
64
  (@field_def.ast_node&.arguments || []).any? do |argument|
62
65
  args.key?(argument.name.underscore.to_sym) && argument.directives.any? { |d| filter_directive?(registry, d) }
@@ -68,8 +71,8 @@ module Lighthouse::GraphQL::DirectiveResolvers
68
71
  registry.arg_builder_class(directive.name) || CONDITION_DIRECTIVES.include?(directive.name)
69
72
  end
70
73
 
71
- def constrained(relation, args)
72
- relation = apply_where_filters(scoped(relation), args)
74
+ def constrained(relation, args, ctx)
75
+ relation = apply_where_filters(scoped(relation), args, ctx)
73
76
  self.class.collection? ? relation : relation.first
74
77
  end
75
78
 
@@ -18,12 +18,7 @@ class Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
18
18
 
19
19
  args_h = args.respond_to?(:to_h) ? args.to_h : (args || {})
20
20
 
21
- relation = apply_where_conditions(relation, args_h)
22
- @field_def.ast_node.arguments.each do |arg_node|
23
- relation = apply_where_has_conditions(relation, arg_node, args_h)
24
- end
25
-
26
- apply_arg_builders(relation, args_h, ctx)
21
+ apply_field_builders(apply_arg_builders(relation, args_h, ctx), args_h, ctx)
27
22
  rescue ::GraphQL::ExecutionError
28
23
  raise
29
24
  rescue StandardError => e
@@ -32,7 +27,7 @@ class Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
32
27
  end
33
28
 
34
29
  # Compose argument-builder directives (@eq, @neq, @in, @notIn, @like,
35
- # @where, @orderBy) onto the relation, in SDL argument order, for each
30
+ # @where, @orderBy, @whereConditions, @whereHasConditions) onto the relation, in SDL argument order, for each
36
31
  # field argument that carries one and that the client supplied a value
37
32
  # for — and for the input fields of an argument carrying @nest.
38
33
  def apply_arg_builders(relation, args_h, ctx = nil)
@@ -51,18 +46,6 @@ class Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
51
46
  relation
52
47
  end
53
48
 
54
- def extract_relation_name_from_directive(arg_node)
55
- directive = arg_node.directives.find { |dir| dir.name == 'whereHasConditions' }
56
- return nil unless directive
57
-
58
- relation_arg = directive.arguments.find { |arg| arg.name == 'relation' }
59
- return nil unless relation_arg
60
-
61
- relation_arg.value.respond_to?(:value) ? relation_arg.value.value : relation_arg.value
62
- rescue StandardError
63
- nil
64
- end
65
-
66
49
  def constantize_model(name)
67
50
  Lighthouse::Support::ModelResolver.constantize(name)
68
51
  end
@@ -113,24 +96,10 @@ class Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
113
96
  @field_def.type.unwrap.graphql_name.sub(/(SimplePaginator|Paginator|Connection)\z/, '')
114
97
  end
115
98
 
116
- # The `where:` argument (@whereConditions), when the client supplied one.
117
- def apply_where_conditions(relation, args_h)
118
- where_condition = args_h[:where] || args_h['where']
119
- return relation unless where_condition
120
-
121
- Lighthouse::GraphQL::DirectiveResolvers::WhereConditionsApplier.apply(relation, where_condition)
122
- end
123
-
124
- # A `whereHasConditions` argument the client supplied filters the
125
- # relation by the related records matching its condition.
126
- def apply_where_has_conditions(relation, arg_node, args_h)
127
- return relation unless arg_node.directives.any? { |dir| dir.name == 'whereHasConditions' }
128
-
129
- arg_value = Lighthouse::Support::Naming.fetch_arg(args_h, arg_node.name)
130
- return relation unless arg_value
131
-
132
- relation_name = extract_relation_name_from_directive(arg_node) || Lighthouse::Support::Naming.derive_relation_name(arg_node.name)
133
- Lighthouse::GraphQL::DirectiveResolvers::WhereConditionsApplier.apply_has_argument(relation, arg_node.name, arg_value, relation_name)
99
+ # The field's own query directives (@builder, @whereAuth, @orderBy(column:)),
100
+ # in SDL order, after the arguments', as PHP Lighthouse's enhanceBuilder applies them.
101
+ def apply_field_builders(relation, args_h = {}, ctx = nil)
102
+ Lighthouse::GraphQL::DirectiveResolvers::BuilderApplier.apply(relation, @field_def.ast_node, args_h, ctx, self)
134
103
  end
135
104
 
136
105
  # Every (argument, directive, builder class) on the field, in SDL order.