lighthouse-graphql 0.6.0 → 0.7.1

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +107 -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 +2 -2
  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/relay.md +52 -3
  16. data/docs/resolvers.md +2 -2
  17. data/docs/schema-organisation.md +177 -0
  18. data/lib/generators/lighthouse/install/graphql_ruby_install.rb +14 -0
  19. data/lib/generators/lighthouse/install/install_generator.rb +31 -12
  20. data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +2 -1
  21. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +11 -4
  22. data/lib/generators/lighthouse/install/templates/schema.graphql.tt +30 -0
  23. data/lib/lighthouse/ast_builder.rb +124 -0
  24. data/lib/lighthouse/config.rb +23 -4
  25. data/lib/lighthouse/contracts/arg_manipulator.rb +9 -5
  26. data/lib/lighthouse/contracts/field_builder.rb +13 -0
  27. data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +1 -2
  28. data/lib/lighthouse/directive_resolvers/all_resolver.rb +0 -2
  29. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +9 -6
  30. data/lib/lighthouse/directive_resolvers/base.rb +6 -37
  31. data/lib/lighthouse/directive_resolvers/builder_applier.rb +21 -6
  32. data/lib/lighthouse/directive_resolvers/count_resolver.rb +0 -1
  33. data/lib/lighthouse/directive_resolvers/first_resolver.rb +0 -2
  34. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +2 -3
  35. data/lib/lighthouse/directives/arguments/order_by.rb +190 -26
  36. data/lib/lighthouse/directives/arguments/where_conditions.rb +25 -0
  37. data/lib/lighthouse/directives/arguments/where_conditions_base.rb +84 -0
  38. data/lib/lighthouse/directives/arguments/where_has_conditions.rb +56 -0
  39. data/lib/lighthouse/directives/definitions.rb +0 -1
  40. data/lib/lighthouse/directives/generated_types.rb +103 -0
  41. data/lib/lighthouse/directives/registry.rb +39 -10
  42. data/lib/lighthouse/document_ast.rb +111 -0
  43. data/lib/lighthouse/graphql/version.rb +1 -1
  44. data/lib/lighthouse/ide_helper.rb +80 -23
  45. data/lib/lighthouse/namespaces.rb +1 -0
  46. data/lib/lighthouse/order_by/types.rb +137 -0
  47. data/lib/lighthouse/railtie.rb +9 -2
  48. data/lib/lighthouse/rb_lighthouse.rb +15 -5
  49. data/lib/lighthouse/resolve_info.rb +5 -5
  50. data/lib/lighthouse/schema_cache.rb +8 -4
  51. data/lib/lighthouse/schema_factory.rb +6 -9
  52. data/lib/lighthouse/schema_generator.rb +4 -15
  53. data/lib/lighthouse/schema_stitcher.rb +64 -0
  54. data/lib/lighthouse/support/naming.rb +0 -12
  55. data/lib/lighthouse/where_conditions/columns.rb +36 -0
  56. data/lib/lighthouse/where_conditions/handler.rb +152 -0
  57. data/lib/lighthouse/where_conditions/json_column.rb +99 -0
  58. data/lib/lighthouse/where_conditions/operator_map.rb +1 -4
  59. data/lib/lighthouse/where_conditions/types.rb +110 -0
  60. data/sig/lighthouse/directives.rbs +16 -3
  61. data/sig/lighthouse/graphql.rbs +48 -4
  62. data/sig/lighthouse.rbs +8 -2
  63. metadata +15 -4
  64. data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +0 -170
  65. data/lib/lighthouse/directives/where_conditions_directive.rb +0 -244
  66. 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.
@@ -26,6 +26,10 @@ module Lighthouse::Generators::GraphqlRubyInstall
26
26
  # The example field graphql-ruby's install puts in each root type.
27
27
  TEST_FIELD = /\n+[ \t]*# TODO: remove me\n[ \t]*field :test_field,.*?\n[ \t]*def test_field\n.*?\n[ \t]*end\n/m
28
28
 
29
+ # The `node` and `nodes` fields graphql-ruby's install (--relay, its default)
30
+ # puts in Types::QueryType, without the methods that resolve them.
31
+ RELAY_FIELDS = /^[ \t]*field :nodes?, \[?Types::NodeType\b.*? do\n(?:[ \t]*argument .*\n)*[ \t]*end\n\n?/
32
+
29
33
  # The `resolve_type` stub graphql-ruby's install puts in the schema class.
30
34
  RESOLVE_TYPE_STUB = /
31
35
  \n+[ \t]*\#\ Union\ and\ Interface\ Resolution\n
@@ -98,6 +102,16 @@ module Lighthouse::Generators::GraphqlRubyInstall
98
102
  options[:subscriptions] || exist?(File.join(directory, 'subscriptions', 'base_subscription.rb'))
99
103
  end
100
104
 
105
+ # Did graphql-ruby's install write Relay's side (--relay, its default):
106
+ # Types::NodeType, and Types::QueryType's node and nodes, as it writes them?
107
+ def relay?
108
+ query_type = types_path('query_type.rb')
109
+ return false unless exist?(types_path('node_type.rb')) && exist?(query_type)
110
+
111
+ source = read(query_type)
112
+ source.scan(RELAY_FIELDS).size == 2 && source.include?('def node(id:)') && source.include?('def nodes(ids:)')
113
+ end
114
+
101
115
  def graphql_ruby_controller?(source)
102
116
  source.include?('def prepare_variables(variables_param)') && source.include?('def handle_error_in_development(e)') &&
103
117
  source.include?("#{schema_name}.execute(query, variables: variables, context: context, operation_name: operation_name)")
@@ -21,6 +21,9 @@ require_relative 'graphql_ruby_install'
21
21
  # graphql-ruby's and stays, but the `resolve_type` stub (Lighthouse resolves
22
22
  # types) and the root types it is built on;
23
23
  # - the example `test_field` goes from the root types;
24
+ # - with Relay, the starter SDL declares Query.node and nodes (and a User
25
+ # @node with global ids), so their `field`s go from Types::QueryType, whose
26
+ # #node / #nodes methods resolve the SDL's fields;
24
27
  # - graphql-ruby's base object, interface, union and scalar include
25
28
  # Lighthouse::GraphQL::Type, so every type class (Types::QueryType,
26
29
  # Types::NodeType, what `graphql:object` writes) is Lighthouse's;
@@ -44,8 +47,8 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
44
47
 
45
48
  class_option :schema, type: :string,
46
49
  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'
50
+ class_option :schema_path, type: :string, default: 'graphql/schema.graphql',
51
+ desc: "The schema's entrypoint, relative to the app root; it #imports the other .graphql files"
49
52
  class_option :subscriptions, type: :boolean, default: false,
50
53
  desc: 'Add Subscriptions::BaseSubscription, the base of the classes Subscription fields resolve to'
51
54
  class_option :skip_controller, type: :boolean, default: false,
@@ -71,12 +74,13 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
71
74
  end
72
75
 
73
76
  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
77
+ if File.exist?(File.join(destination_root, sdl_schema_path))
78
+ say_status :skip, "#{sdl_schema_path} exists", :yellow
76
79
  return
77
80
  end
78
81
 
79
- template 'schema.graphql.tt', File.join(sdl_folder, 'schema.graphql')
82
+ @relay_starter = relay?
83
+ template 'schema.graphql.tt', sdl_schema_path
80
84
  end
81
85
 
82
86
  # Subscriptions::BaseSubscription, which graphql-ruby has none of (see BaseClasses).
@@ -101,12 +105,15 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
101
105
  add_dataloader if source.include?('use GraphQL::Batch') && source.exclude?('use GraphQL::Dataloader')
102
106
  end
103
107
 
104
- # The example field goes; the root types' own fields stay.
108
+ # The example field goes; the root types' own fields stay. With Relay, the
109
+ # starter SDL declares Query.node and nodes, so their `field`s go too, and
110
+ # the methods graphql-ruby wrote resolve the SDL's fields.
105
111
  def adapt_root_types
106
112
  %w[query_type mutation_type subscription_type].each do |name|
107
113
  path = types_path("#{name}.rb")
108
114
  gsub_file path, TEST_FIELD, "\n" if exist?(path) && read(path).match?(TEST_FIELD)
109
115
  end
116
+ move_relay_fields_to_sdl if @relay_starter
110
117
  end
111
118
 
112
119
  # Every type class is Lighthouse's: graphql-ruby's base object, interface,
@@ -174,14 +181,14 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
174
181
  say <<~MSG
175
182
 
176
183
  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).
184
+ 1. Describe your API in #{sdl_schema_path} (the starter assumes a User model); #import more files, which `extend type Query`.
178
185
  2. Resolve a field in Ruby when a directive is not enough, as in graphql-ruby: a method of
179
186
  Types::QueryType (Query.latestPosts -> #latest_posts), a `field` of its own there,
180
187
  `bin/rails g graphql:mutation`, or a Resolvers::BaseResolver subclass named after the field.
181
188
  3. Give a type Ruby methods, a union or interface its resolve_type, a scalar its coercion:
182
189
  bin/rails g graphql:object Post (or graphql:union, graphql:interface, graphql:scalar);
183
190
  the class is Lighthouse's through its base (Lighthouse::GraphQL::Type)
184
- 4. Generate the IDE helper files: bin/rails lighthouse:ide_helper
191
+ 4. The IDE helper files are rewritten in development; or: bin/rails lighthouse:ide_helper
185
192
  5. #{serve}
186
193
  MSG
187
194
  end
@@ -196,7 +203,7 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
196
203
 
197
204
  def schema_comment(indent)
198
205
  <<~RUBY.gsub(/^/, indent).delete_prefix(indent)
199
- # Lighthouse builds the schema from the SDL in #{sdl_folder}/, on the classes graphql-ruby's
206
+ # Lighthouse builds the schema from the SDL in #{sdl_schema_path}, on the classes graphql-ruby's
200
207
  # install wrote (base_types): the SDL's root types extend the code-first ones, whose
201
208
  # `field`s stay the schema's, and every type, resolver and mutation class inherits from
202
209
  # its kind's base. The rest of the class is graphql-ruby's.
@@ -210,12 +217,24 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
210
217
  "#{indent}use GraphQL::Dataloader\n", after: anchor
211
218
  end
212
219
 
220
+ # The SDL's entrypoint (Config.schema_path); `schema_path` is graphql-ruby's schema class.
221
+ def sdl_schema_path
222
+ options[:schema_path].delete_prefix('/')
223
+ end
224
+
213
225
  def sdl_folder
214
- options[:sdl_folder].delete_suffix('/')
226
+ File.dirname(sdl_schema_path)
227
+ end
228
+
229
+ def default_sdl_schema_path?
230
+ sdl_schema_path == 'graphql/schema.graphql'
215
231
  end
216
232
 
217
- def default_sdl_folder?
218
- sdl_folder == 'graphql'
233
+ def move_relay_fields_to_sdl
234
+ path = types_path('query_type.rb')
235
+ gsub_file path, RELAY_FIELDS, ''
236
+ gsub_file path, /^([ \t]*)def node\(id:\)\n/,
237
+ "\\1# Query.node and nodes are declared in #{sdl_schema_path}; these methods resolve them.\n\\1def node(id:)\n"
219
238
  end
220
239
 
221
240
  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
@@ -1,15 +1,44 @@
1
1
  # Starter schema generated by `rails generate lighthouse:install`. It assumes a
2
2
  # User model with `name` and `email` columns — replace it with your own types.
3
+ # This file is the schema's entrypoint: split the schema into files it imports
4
+ # (#import user.graphql), which add their fields with `extend type Query`.
3
5
  # Directives reference: https://github.com/Vaz-Innovation/lighthouse-graphql/blob/main/docs/directives.md
4
6
 
5
7
  "Indicates what fields are available at the top level of a query operation."
6
8
  type Query {
9
+ <% if relay? -%>
10
+ "Fetches an object given its ID. Resolved by Types::QueryType#node, with <%= schema_name %>.object_from_id."
11
+ node("ID of the object." id: ID!): Node
12
+
13
+ "Fetches a list of objects given a list of IDs. Resolved by Types::QueryType#nodes."
14
+ nodes("IDs of the objects." ids: [ID!]!): [Node]
15
+
16
+ "Find a single user by its global id."
17
+ user(id: ID! @globalId(decode: ID) @eq): User @find
18
+ <% else -%>
7
19
  "Find a single user by id."
8
20
  user(id: ID! @eq): User @find
21
+ <% end -%>
9
22
 
10
23
  "List users, a page at a time (10 by default; pass first: and page:)."
11
24
  users: [User!]! @paginate(defaultCount: 10)
12
25
  }
26
+ <% if relay? -%>
27
+
28
+ "An object with an ID: graphql-ruby's Types::NodeType."
29
+ interface Node {
30
+ "ID of the object."
31
+ id: ID!
32
+ }
33
+
34
+ "Account of a person who uses this application. @node makes it a Node, which Query.node finds."
35
+ type User @node {
36
+ "Its global id, from <%= schema_name %>.id_from_object (Rails' GlobalID)."
37
+ id: ID! @globalId
38
+ name: String
39
+ email: String
40
+ }
41
+ <% else -%>
13
42
 
14
43
  "Account of a person who uses this application."
15
44
  type User {
@@ -17,3 +46,4 @@ type User {
17
46
  name: String
18
47
  email: String
19
48
  }
49
+ <% end -%>
@@ -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]