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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +95 -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 +1 -1
- 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/resolvers.md +2 -2
- data/docs/schema-organisation.md +177 -0
- data/lib/generators/lighthouse/install/install_generator.rb +16 -11
- 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/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.
|
|
@@ -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 :
|
|
48
|
-
|
|
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
|
|
75
|
-
say_status :skip, "#{
|
|
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',
|
|
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 #{
|
|
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.
|
|
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 #{
|
|
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
|
-
|
|
219
|
+
File.dirname(sdl_schema_path)
|
|
215
220
|
end
|
|
216
221
|
|
|
217
|
-
def
|
|
218
|
-
|
|
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
|
|
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
|
|
@@ -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]
|
|
@@ -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
|
|
56
|
-
#
|
|
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
|
-
|
|
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
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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.
|