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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +256 -1
  3. data/README.md +26 -15
  4. data/docs/caching.md +4 -3
  5. data/docs/configuration.md +16 -12
  6. data/docs/directives.md +294 -4
  7. data/docs/generators.md +84 -0
  8. data/docs/getting-started.md +93 -60
  9. data/docs/performance.md +2 -2
  10. data/docs/relationships.md +47 -4
  11. data/docs/relay.md +200 -0
  12. data/docs/resolvers.md +326 -34
  13. data/docs/security.md +73 -8
  14. data/docs/subscriptions.md +16 -9
  15. data/docs/types.md +218 -0
  16. data/docs/uploads.md +107 -0
  17. data/lib/generators/lighthouse/base_classes.rb +27 -49
  18. data/lib/generators/lighthouse/install/graphql_ruby_install.rb +113 -0
  19. data/lib/generators/lighthouse/install/install_generator.rb +125 -45
  20. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +10 -8
  21. data/lib/lighthouse/async_mutation_job.rb +17 -0
  22. data/lib/lighthouse/caching.rb +9 -3
  23. data/lib/lighthouse/complexity.rb +5 -4
  24. data/lib/lighthouse/config.rb +12 -18
  25. data/lib/lighthouse/controller.rb +53 -12
  26. data/lib/lighthouse/dataloader_sources/preload_loader.rb +16 -0
  27. data/lib/lighthouse/dataloader_sources/relation_counts_loader.rb +46 -0
  28. data/lib/lighthouse/default_resolve_dispatcher.rb +26 -0
  29. data/lib/lighthouse/directive_resolvers/all_resolver.rb +1 -1
  30. data/lib/lighthouse/directive_resolvers/argument_binding.rb +61 -0
  31. data/lib/lighthouse/directive_resolvers/argument_transforms_resolver.rb +220 -0
  32. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +2 -2
  33. data/lib/lighthouse/directive_resolvers/async_resolver.rb +90 -0
  34. data/lib/lighthouse/directive_resolvers/base.rb +63 -14
  35. data/lib/lighthouse/directive_resolvers/builder_applier.rb +89 -19
  36. data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +1 -1
  37. data/lib/lighthouse/directive_resolvers/can_resolvers.rb +1 -1
  38. data/lib/lighthouse/directive_resolvers/convention_resolver.rb +2 -1
  39. data/lib/lighthouse/directive_resolvers/count_resolver.rb +1 -1
  40. data/lib/lighthouse/directive_resolvers/field_resolver.rb +35 -7
  41. data/lib/lighthouse/directive_resolvers/first_resolver.rb +1 -1
  42. data/lib/lighthouse/directive_resolvers/global_id_resolver.rb +59 -0
  43. data/lib/lighthouse/directive_resolvers/lazy_load_resolver.rb +61 -0
  44. data/lib/lighthouse/directive_resolvers/limit_resolver.rb +59 -0
  45. data/lib/lighthouse/directive_resolvers/method_resolver.rb +38 -3
  46. data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +1 -1
  47. data/lib/lighthouse/directive_resolvers/namespaced_resolver.rb +31 -0
  48. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +1 -1
  49. data/lib/lighthouse/directive_resolvers/resolution.rb +19 -4
  50. data/lib/lighthouse/directive_resolvers/subscription_resolver.rb +2 -1
  51. data/lib/lighthouse/directive_resolvers/throttle_resolver.rb +136 -0
  52. data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +87 -0
  53. data/lib/lighthouse/directive_resolvers/with_count_resolver.rb +43 -0
  54. data/lib/lighthouse/directive_resolvers/with_resolver.rb +42 -0
  55. data/lib/lighthouse/directives/arguments/more_filters.rb +215 -0
  56. data/lib/lighthouse/directives/definitions.rb +52 -0
  57. data/lib/lighthouse/directives/model_directive.rb +131 -0
  58. data/lib/lighthouse/directives/node_directive.rb +134 -0
  59. data/lib/lighthouse/directives/pagination_manipulator.rb +1 -1
  60. data/lib/lighthouse/directives/registry.rb +21 -21
  61. data/lib/lighthouse/directives/visibility_directive.rb +71 -0
  62. data/lib/lighthouse/enum_directive.rb +41 -0
  63. data/lib/lighthouse/field_resolver_stack.rb +56 -6
  64. data/lib/lighthouse/generated_resolvers.rb +13 -8
  65. data/lib/lighthouse/global_id.rb +57 -0
  66. data/lib/lighthouse/graphql/version.rb +1 -1
  67. data/lib/lighthouse/graphql_ruby_patches.rb +45 -0
  68. data/lib/lighthouse/multipart_request.rb +43 -0
  69. data/lib/lighthouse/node_registry.rb +68 -0
  70. data/lib/lighthouse/pagination.rb +3 -0
  71. data/lib/lighthouse/rb_lighthouse.rb +34 -0
  72. data/lib/lighthouse/relation_counts.rb +29 -0
  73. data/lib/lighthouse/resolve_info.rb +80 -0
  74. data/lib/lighthouse/resolver_bases.rb +25 -4
  75. data/lib/lighthouse/resolver_dispatch.rb +51 -11
  76. data/lib/lighthouse/root_types.rb +117 -0
  77. data/lib/lighthouse/scalar_coercion.rb +72 -0
  78. data/lib/lighthouse/schema_cache.rb +2 -1
  79. data/lib/lighthouse/schema_generator.rb +13 -11
  80. data/lib/lighthouse/schema_implementation.rb +4 -8
  81. data/lib/lighthouse/support/environment.rb +13 -0
  82. data/lib/lighthouse/support/model_resolver.rb +7 -5
  83. data/lib/lighthouse/support/preload.rb +49 -0
  84. data/lib/lighthouse/type.rb +103 -0
  85. data/lib/lighthouse/type_resolution.rb +150 -0
  86. data/lib/lighthouse/upload.rb +28 -0
  87. data/lib/lighthouse/upload_store.rb +57 -0
  88. data/sig/lighthouse/directives.rbs +3 -2
  89. data/sig/lighthouse.rbs +10 -2
  90. metadata +40 -4
  91. data/lib/generators/lighthouse/install/templates/schema.rb.tt +0 -22
  92. data/lib/generators/lighthouse/mutation_generator.rb +0 -31
  93. 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 constant living
65
- in an autoloaded file, as the generated `app/graphql/app_schema.rb` does.
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
 
@@ -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 (the install generator does):
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 = Lighthouse::GraphQL::RbLightHouse.get_schema(
42
- options: { using: { GraphQL::Dataloader => {} } }
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
+ ```