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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +107 -1
- data/README.md +15 -7
- data/docs/best-practices.md +6 -4
- data/docs/configuration.md +19 -5
- data/docs/custom-directives.md +2 -1
- data/docs/directives.md +65 -7
- data/docs/federation.md +1 -1
- data/docs/filtering-and-ordering.md +330 -30
- data/docs/generators.md +2 -2
- data/docs/getting-started.md +43 -10
- data/docs/motivation.md +3 -2
- data/docs/performance.md +4 -3
- data/docs/relationships.md +2 -1
- data/docs/relay.md +52 -3
- data/docs/resolvers.md +2 -2
- data/docs/schema-organisation.md +177 -0
- data/lib/generators/lighthouse/install/graphql_ruby_install.rb +14 -0
- data/lib/generators/lighthouse/install/install_generator.rb +31 -12
- data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +2 -1
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +11 -4
- data/lib/generators/lighthouse/install/templates/schema.graphql.tt +30 -0
- data/lib/lighthouse/ast_builder.rb +124 -0
- data/lib/lighthouse/config.rb +23 -4
- data/lib/lighthouse/contracts/arg_manipulator.rb +9 -5
- data/lib/lighthouse/contracts/field_builder.rb +13 -0
- data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +1 -2
- data/lib/lighthouse/directive_resolvers/all_resolver.rb +0 -2
- data/lib/lighthouse/directive_resolvers/association_resolvers.rb +9 -6
- data/lib/lighthouse/directive_resolvers/base.rb +6 -37
- data/lib/lighthouse/directive_resolvers/builder_applier.rb +21 -6
- data/lib/lighthouse/directive_resolvers/count_resolver.rb +0 -1
- data/lib/lighthouse/directive_resolvers/first_resolver.rb +0 -2
- data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +2 -3
- data/lib/lighthouse/directives/arguments/order_by.rb +190 -26
- data/lib/lighthouse/directives/arguments/where_conditions.rb +25 -0
- data/lib/lighthouse/directives/arguments/where_conditions_base.rb +84 -0
- data/lib/lighthouse/directives/arguments/where_has_conditions.rb +56 -0
- data/lib/lighthouse/directives/definitions.rb +0 -1
- data/lib/lighthouse/directives/generated_types.rb +103 -0
- data/lib/lighthouse/directives/registry.rb +39 -10
- data/lib/lighthouse/document_ast.rb +111 -0
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/lib/lighthouse/ide_helper.rb +80 -23
- data/lib/lighthouse/namespaces.rb +1 -0
- data/lib/lighthouse/order_by/types.rb +137 -0
- data/lib/lighthouse/railtie.rb +9 -2
- data/lib/lighthouse/rb_lighthouse.rb +15 -5
- data/lib/lighthouse/resolve_info.rb +5 -5
- data/lib/lighthouse/schema_cache.rb +8 -4
- data/lib/lighthouse/schema_factory.rb +6 -9
- data/lib/lighthouse/schema_generator.rb +4 -15
- data/lib/lighthouse/schema_stitcher.rb +64 -0
- data/lib/lighthouse/support/naming.rb +0 -12
- data/lib/lighthouse/where_conditions/columns.rb +36 -0
- data/lib/lighthouse/where_conditions/handler.rb +152 -0
- data/lib/lighthouse/where_conditions/json_column.rb +99 -0
- data/lib/lighthouse/where_conditions/operator_map.rb +1 -4
- data/lib/lighthouse/where_conditions/types.rb +110 -0
- data/sig/lighthouse/directives.rbs +16 -3
- data/sig/lighthouse/graphql.rbs +48 -4
- data/sig/lighthouse.rbs +8 -2
- metadata +15 -4
- data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +0 -170
- data/lib/lighthouse/directives/where_conditions_directive.rb +0 -244
- 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 :
|
|
48
|
-
|
|
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
|
|
75
|
-
say_status :skip, "#{
|
|
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
|
-
|
|
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 #{
|
|
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.
|
|
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 #{
|
|
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
|
-
|
|
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
|
|
218
|
-
|
|
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
|
|
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
|
-
#
|
|
7
|
-
|
|
8
|
-
#
|
|
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.
|
|
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
|
data/lib/lighthouse/config.rb
CHANGED
|
@@ -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
|
-
|
|
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 :
|
|
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
|
-
|
|
44
|
-
|
|
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
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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
|
|
10
|
-
#
|
|
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 =
|
|
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]
|