lighthouse-graphql 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +256 -1
- data/README.md +26 -15
- data/docs/caching.md +4 -3
- data/docs/configuration.md +16 -12
- data/docs/directives.md +294 -4
- data/docs/generators.md +84 -0
- data/docs/getting-started.md +93 -60
- data/docs/performance.md +2 -2
- data/docs/relationships.md +47 -4
- data/docs/relay.md +200 -0
- data/docs/resolvers.md +326 -34
- data/docs/security.md +73 -8
- data/docs/subscriptions.md +16 -9
- data/docs/types.md +218 -0
- data/docs/uploads.md +107 -0
- data/lib/generators/lighthouse/base_classes.rb +27 -49
- data/lib/generators/lighthouse/install/graphql_ruby_install.rb +113 -0
- data/lib/generators/lighthouse/install/install_generator.rb +125 -45
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +10 -8
- data/lib/lighthouse/async_mutation_job.rb +17 -0
- data/lib/lighthouse/caching.rb +9 -3
- data/lib/lighthouse/complexity.rb +5 -4
- data/lib/lighthouse/config.rb +12 -18
- data/lib/lighthouse/controller.rb +53 -12
- data/lib/lighthouse/dataloader_sources/preload_loader.rb +16 -0
- data/lib/lighthouse/dataloader_sources/relation_counts_loader.rb +46 -0
- data/lib/lighthouse/default_resolve_dispatcher.rb +26 -0
- data/lib/lighthouse/directive_resolvers/all_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/argument_binding.rb +61 -0
- data/lib/lighthouse/directive_resolvers/argument_transforms_resolver.rb +220 -0
- data/lib/lighthouse/directive_resolvers/association_resolvers.rb +2 -2
- data/lib/lighthouse/directive_resolvers/async_resolver.rb +90 -0
- data/lib/lighthouse/directive_resolvers/base.rb +63 -14
- data/lib/lighthouse/directive_resolvers/builder_applier.rb +89 -19
- data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +1 -1
- data/lib/lighthouse/directive_resolvers/can_resolvers.rb +1 -1
- data/lib/lighthouse/directive_resolvers/convention_resolver.rb +2 -1
- data/lib/lighthouse/directive_resolvers/count_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/field_resolver.rb +35 -7
- data/lib/lighthouse/directive_resolvers/first_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/global_id_resolver.rb +59 -0
- data/lib/lighthouse/directive_resolvers/lazy_load_resolver.rb +61 -0
- data/lib/lighthouse/directive_resolvers/limit_resolver.rb +59 -0
- data/lib/lighthouse/directive_resolvers/method_resolver.rb +38 -3
- data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/namespaced_resolver.rb +31 -0
- data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/resolution.rb +19 -4
- data/lib/lighthouse/directive_resolvers/subscription_resolver.rb +2 -1
- data/lib/lighthouse/directive_resolvers/throttle_resolver.rb +136 -0
- data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +87 -0
- data/lib/lighthouse/directive_resolvers/with_count_resolver.rb +43 -0
- data/lib/lighthouse/directive_resolvers/with_resolver.rb +42 -0
- data/lib/lighthouse/directives/arguments/more_filters.rb +215 -0
- data/lib/lighthouse/directives/definitions.rb +52 -0
- data/lib/lighthouse/directives/model_directive.rb +131 -0
- data/lib/lighthouse/directives/node_directive.rb +134 -0
- data/lib/lighthouse/directives/pagination_manipulator.rb +1 -1
- data/lib/lighthouse/directives/registry.rb +21 -21
- data/lib/lighthouse/directives/visibility_directive.rb +71 -0
- data/lib/lighthouse/enum_directive.rb +41 -0
- data/lib/lighthouse/field_resolver_stack.rb +56 -6
- data/lib/lighthouse/generated_resolvers.rb +13 -8
- data/lib/lighthouse/global_id.rb +57 -0
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/lib/lighthouse/graphql_ruby_patches.rb +45 -0
- data/lib/lighthouse/multipart_request.rb +43 -0
- data/lib/lighthouse/node_registry.rb +68 -0
- data/lib/lighthouse/pagination.rb +3 -0
- data/lib/lighthouse/rb_lighthouse.rb +34 -0
- data/lib/lighthouse/relation_counts.rb +29 -0
- data/lib/lighthouse/resolve_info.rb +80 -0
- data/lib/lighthouse/resolver_bases.rb +25 -4
- data/lib/lighthouse/resolver_dispatch.rb +51 -11
- data/lib/lighthouse/root_types.rb +117 -0
- data/lib/lighthouse/scalar_coercion.rb +72 -0
- data/lib/lighthouse/schema_cache.rb +2 -1
- data/lib/lighthouse/schema_generator.rb +13 -11
- data/lib/lighthouse/schema_implementation.rb +4 -8
- data/lib/lighthouse/support/environment.rb +13 -0
- data/lib/lighthouse/support/model_resolver.rb +7 -5
- data/lib/lighthouse/support/preload.rb +49 -0
- data/lib/lighthouse/type.rb +103 -0
- data/lib/lighthouse/type_resolution.rb +150 -0
- data/lib/lighthouse/upload.rb +28 -0
- data/lib/lighthouse/upload_store.rb +57 -0
- data/sig/lighthouse/directives.rbs +3 -2
- data/sig/lighthouse.rbs +10 -2
- metadata +40 -4
- data/lib/generators/lighthouse/install/templates/schema.rb.tt +0 -22
- data/lib/generators/lighthouse/mutation_generator.rb +0 -31
- data/lib/generators/lighthouse/query_generator.rb +0 -30
data/docs/performance.md
CHANGED
|
@@ -61,8 +61,8 @@ Lighthouse.configure { |config| config.watch_sdl = Rails.env.development? }
|
|
|
61
61
|
Rails' reloader watches Ruby files, not `.graphql` ones, so without this an SDL
|
|
62
62
|
edit only shows up after restarting the server. With it, the SDL folder joins
|
|
63
63
|
`config.watchable_dirs`, and an edit reloads the app, and with it the schema, on
|
|
64
|
-
the next request, like a Ruby change. This relies on the schema
|
|
65
|
-
in an autoloaded file, as
|
|
64
|
+
the next request, like a Ruby change. This relies on the schema class living
|
|
65
|
+
in an autoloaded file, as `app/graphql/app_schema.rb` does.
|
|
66
66
|
|
|
67
67
|
## Persisted queries
|
|
68
68
|
|
data/docs/relationships.md
CHANGED
|
@@ -31,16 +31,21 @@ The `morph*` names are PHP Lighthouse's. ActiveRecord resolves polymorphic
|
|
|
31
31
|
associations the same way as any other, so they behave exactly like their
|
|
32
32
|
non-polymorphic counterparts. Use whichever reads better.
|
|
33
33
|
|
|
34
|
+
`@hasMany` and `@hasOne` resolve `has_many` / `has_one :through` associations as
|
|
35
|
+
they are. PHP Lighthouse's `@hasManyThrough` and `@hasOneThrough` work too, as
|
|
36
|
+
aliases, so an SDL written for it builds unchanged.
|
|
37
|
+
|
|
34
38
|
## Avoiding the N+1 problem
|
|
35
39
|
|
|
36
40
|
The relationship directives load through graphql-ruby's **Dataloader** and
|
|
37
41
|
ActiveRecord's preloader, one query per association per batch. Enable the
|
|
38
|
-
Dataloader on your schema (
|
|
42
|
+
Dataloader on your schema (graphql-ruby's install does, and with `--batch`
|
|
43
|
+
`lighthouse:install` adds it beside GraphQL::Batch):
|
|
39
44
|
|
|
40
45
|
```ruby
|
|
41
|
-
AppSchema
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
class AppSchema < Lighthouse::GraphQL::RbLightHouse.get_schema(options: { base_types: { object: Types::BaseObject } })
|
|
47
|
+
use GraphQL::Dataloader
|
|
48
|
+
end
|
|
44
49
|
```
|
|
45
50
|
|
|
46
51
|
A query for 100 posts and their authors then issues two queries, not 101. A
|
|
@@ -50,6 +55,44 @@ the Dataloader, the directives still work, but they load one parent at a time.
|
|
|
50
55
|
A field with `scopes:` or a filter argument the client supplied runs its own query
|
|
51
56
|
per parent, because each parent's query differs.
|
|
52
57
|
|
|
58
|
+
## Eager loading for other fields
|
|
59
|
+
|
|
60
|
+
The relationship directives load their own association. Three directives load for
|
|
61
|
+
the fields that only read one: a method, a type class method, a field read off
|
|
62
|
+
the model. Each one loads across every parent in the response at once, through the
|
|
63
|
+
Dataloader:
|
|
64
|
+
|
|
65
|
+
- `@with(relation: "tasks")` loads a relation of the parent before the field
|
|
66
|
+
resolves. `user.tasks` is then in memory for the method that reads it.
|
|
67
|
+
- `@withCount(relation: "posts")` counts a relation for every parent in one
|
|
68
|
+
`GROUP BY` query, and keeps each count on its parent as `<relation>_count`, as
|
|
69
|
+
Laravel's `withCount` does. It is not the field's value: the field's resolver,
|
|
70
|
+
or the default reader of a `postsCount` field, reads `user.posts_count`.
|
|
71
|
+
- `@lazyLoad(relations: ["tags"])` loads relations of the records the field
|
|
72
|
+
returns: a list, a relation, a `@paginate` page. On a `@hasMany`, the records of
|
|
73
|
+
every parent load their relations together.
|
|
74
|
+
|
|
75
|
+
```graphql
|
|
76
|
+
type User {
|
|
77
|
+
latestPostTitle: String @with(relation: "posts") @method
|
|
78
|
+
usage: String! @withCount(relation: "posts", scopes: ["published"]) @method
|
|
79
|
+
posts: [Post!]! @hasMany @lazyLoad(relations: ["tags"])
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
class User < ApplicationRecord
|
|
85
|
+
def latest_post_title = posts.max_by(&:created_at)&.title
|
|
86
|
+
def usage = "#{posts_count} published posts"
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Relations may be nested with dots (`"posts.comments"`), and `scopes` narrow what is
|
|
91
|
+
loaded or counted, as model scopes. A count loads in one query when the database has
|
|
92
|
+
window functions and the relation links its parent through one column, as a
|
|
93
|
+
[paginated relation](#paginating-many-parents-at-once) does; otherwise once per
|
|
94
|
+
parent.
|
|
95
|
+
|
|
53
96
|
## Lists, and paginating them
|
|
54
97
|
|
|
55
98
|
Collection directives return a **plain list**, as in PHP Lighthouse:
|
data/docs/relay.md
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Relay: Global Object Identification
|
|
2
|
+
|
|
3
|
+
Relay clients refetch any object by one opaque id, through a root `node` field.
|
|
4
|
+
Lighthouse gives a schema that, as [PHP Lighthouse](https://lighthouse-php.com/master/digging-deeper/relay.html)
|
|
5
|
+
does, with two directives: `@globalId` encodes and decodes the ids, and `@node`
|
|
6
|
+
makes a type findable by them. For Relay's cursor pagination, see
|
|
7
|
+
[`@paginate(type: CONNECTION)`](directives.md#paginate).
|
|
8
|
+
|
|
9
|
+
An app whose schema has graphql-ruby's Relay, as `rails g graphql:install` writes it
|
|
10
|
+
by default, uses the same directives on graphql-ruby's ids and `Node`: see
|
|
11
|
+
[Relay through graphql-ruby](#relay-through-graphql-ruby). The sections before it
|
|
12
|
+
describe a schema without it.
|
|
13
|
+
|
|
14
|
+
## Global ids
|
|
15
|
+
|
|
16
|
+
`@globalId` on a field encodes its value with the type's name:
|
|
17
|
+
|
|
18
|
+
```graphql
|
|
19
|
+
type User {
|
|
20
|
+
id: ID! @globalId
|
|
21
|
+
name: String
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`{ user(id: 1) { id } }` answers `"VXNlcjox"`, the base64 of `User:1`.
|
|
26
|
+
|
|
27
|
+
On an argument, or an input field, `@globalId` decodes what the client sent before
|
|
28
|
+
the field resolves, so every resolver (a directive, `@field`, a `Resolvers::` class)
|
|
29
|
+
gets the decoded value. `decode:` says which part:
|
|
30
|
+
|
|
31
|
+
| `decode:` | The resolver gets |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| `ARRAY` (default) | `["User", "1"]` |
|
|
34
|
+
| `TYPE` | `"User"` |
|
|
35
|
+
| `ID` | `"1"` |
|
|
36
|
+
|
|
37
|
+
```graphql
|
|
38
|
+
type Query {
|
|
39
|
+
user(id: ID! @globalId(decode: ID) @eq): User @find
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type Mutation {
|
|
43
|
+
deleteNode(id: ID! @globalId): Node
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Lists of ids decode item by item. A value that is not a global id makes the field
|
|
48
|
+
fail with `"…" is not a valid global id.`
|
|
49
|
+
|
|
50
|
+
## Nodes
|
|
51
|
+
|
|
52
|
+
`@node` registers a type for refetching:
|
|
53
|
+
|
|
54
|
+
```graphql
|
|
55
|
+
type User @node {
|
|
56
|
+
id: ID! @globalId
|
|
57
|
+
name: String
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
and adds what Relay expects, leaving alone whatever the SDL declares itself:
|
|
62
|
+
|
|
63
|
+
```graphql
|
|
64
|
+
"Any object implementing this type can be found by ID through `Query.node`."
|
|
65
|
+
interface Node {
|
|
66
|
+
"Global identifier that can be used to resolve any Node implementation."
|
|
67
|
+
id: ID!
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type User implements Node @node { ... }
|
|
71
|
+
|
|
72
|
+
type Query {
|
|
73
|
+
"Fetches an object given its globally unique `ID`."
|
|
74
|
+
node(id: ID! @globalId): Node
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```graphql
|
|
79
|
+
{
|
|
80
|
+
node(id: "VXNlcjox") {
|
|
81
|
+
id
|
|
82
|
+
... on User { name }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The type keeps its own `id: ID! @globalId`, which encodes the ids `node` decodes.
|
|
88
|
+
`node` answers `null` for an id with no object, and an error for the id of a type
|
|
89
|
+
without `@node`.
|
|
90
|
+
|
|
91
|
+
### Where the object comes from
|
|
92
|
+
|
|
93
|
+
1. `@node(resolver: "Countries@by_code")` names a class, and a method after the `@`
|
|
94
|
+
(`call` without one). It gets the decoded id and the context, and returns any
|
|
95
|
+
object, a Hash included:
|
|
96
|
+
|
|
97
|
+
```graphql
|
|
98
|
+
type Country @node(resolver: "Countries@by_code") {
|
|
99
|
+
id: ID! @globalId
|
|
100
|
+
name: String!
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
class Resolvers::Countries
|
|
106
|
+
COUNTRIES = { 'DE' => { id: 'DE', name: 'Germany' } }.freeze
|
|
107
|
+
|
|
108
|
+
def self.by_code(id, _context) = COUNTRIES[id]
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The class is looked up as `@field` looks one up on a type: in the resolvers
|
|
113
|
+
namespace, then in [`resolver_namespaces`](configuration.md#resolver_namespaces).
|
|
114
|
+
2. `@node(model: "Account")` names the model.
|
|
115
|
+
3. Otherwise, the type's [`@model`](directives.md#model), or the model named
|
|
116
|
+
after the type.
|
|
117
|
+
|
|
118
|
+
A model's record is found by its primary key, through graphql-ruby's Dataloader, so
|
|
119
|
+
several `node` fields in one query load together.
|
|
120
|
+
|
|
121
|
+
To give `Query.node` your own description or middleware, declare it, with the
|
|
122
|
+
registry Lighthouse uses:
|
|
123
|
+
|
|
124
|
+
```graphql
|
|
125
|
+
type Query {
|
|
126
|
+
"This description is up to you."
|
|
127
|
+
node(id: ID! @globalId): Node
|
|
128
|
+
@field(resolver: "Lighthouse::GraphQL::NodeRegistry@resolve")
|
|
129
|
+
@guard
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Relay through graphql-ruby
|
|
134
|
+
|
|
135
|
+
graphql-ruby's install (`--relay`, its default) writes Relay's side in Ruby:
|
|
136
|
+
`Types::NodeType`, the `node` and `nodes` fields of `Types::QueryType`, and the
|
|
137
|
+
schema's `id_from_object` / `object_from_id`, on Rails' GlobalID:
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
class AppSchema < Lighthouse::GraphQL::RbLightHouse.get_schema(
|
|
141
|
+
options: { base_types: { query_type: Types::QueryType, node: Types::NodeType, object: Types::BaseObject } }
|
|
142
|
+
)
|
|
143
|
+
def self.id_from_object(object, type_definition, query_ctx)
|
|
144
|
+
object.to_gid_param
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def self.object_from_id(global_id, query_ctx)
|
|
148
|
+
GlobalID.find(global_id)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`lighthouse:install` names those classes in `base_types`, and Lighthouse's
|
|
154
|
+
directives then work on them. The schema has one kind of global id and one `Node`:
|
|
155
|
+
|
|
156
|
+
- `@globalId` on a field that resolves an object's own id encodes it with
|
|
157
|
+
`id_from_object`: `Tag.id` answers `tag.to_gid_param`, as graphql-ruby's `Node.id`
|
|
158
|
+
does. Any other value is encoded as before.
|
|
159
|
+
- `@globalId` on an argument decodes with `object_from_id`, which loads the object.
|
|
160
|
+
`ID` gives its id, and `ARRAY` / `TYPE` its class name (`["Tag", 1]`, `"Tag"`).
|
|
161
|
+
- `node:` is the SDL's `Node` interface: `interface Node`, which `@node` adds, and
|
|
162
|
+
`implements Node` refer to `Types::NodeType`. A `@node` type implements it.
|
|
163
|
+
- `Query.node` and `nodes` are graphql-ruby's code-first fields of
|
|
164
|
+
`Types::QueryType`, which find objects with `object_from_id`. The `node` field
|
|
165
|
+
`@node` would add to `Query` gives way to them.
|
|
166
|
+
|
|
167
|
+
```graphql
|
|
168
|
+
type Tag @node {
|
|
169
|
+
id: ID! @globalId
|
|
170
|
+
name: String!
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
type Query {
|
|
174
|
+
tag(id: ID! @globalId(decode: ID) @eq): Tag @find
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```graphql
|
|
179
|
+
{
|
|
180
|
+
node(id: "Z2lkOi8vYXBwL1RhZy8x") { id ... on Tag { name } }
|
|
181
|
+
tag(id: "Z2lkOi8vYXBwL1RhZy8x") { name }
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`Tag.id` and both arguments carry `Tag` 1's GlobalID (`gid://app/Tag/1`, in base64).
|
|
186
|
+
`Query.node` finds every object with `object_from_id`, so `@node(resolver:)` and
|
|
187
|
+
`@node(model:)` play no part in it, and [`global_id`](configuration.md#global_id)
|
|
188
|
+
encodes only what `id_from_object` does not. A schema without `id_from_object` /
|
|
189
|
+
`object_from_id` works as the sections above describe.
|
|
190
|
+
|
|
191
|
+
## Your own encoding
|
|
192
|
+
|
|
193
|
+
[`global_id`](configuration.md#global_id) takes any object with
|
|
194
|
+
`encode(type, id)` and `decode(global_id)`, which returns `[type, id]`:
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
Lighthouse.configure do |config|
|
|
198
|
+
config.global_id = MyGlobalIds # e.g. signed, or a UUID lookup
|
|
199
|
+
end
|
|
200
|
+
```
|