lighthouse-graphql 0.3.0 → 0.5.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 (99) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +182 -1
  3. data/README.md +18 -7
  4. data/docs/best-practices.md +16 -6
  5. data/docs/configuration.md +33 -8
  6. data/docs/directives.md +318 -10
  7. data/docs/events.md +3 -1
  8. data/docs/federation.md +3 -2
  9. data/docs/generators.md +82 -0
  10. data/docs/getting-started.md +66 -32
  11. data/docs/motivation.md +3 -2
  12. data/docs/relationships.md +42 -0
  13. data/docs/relay.md +137 -0
  14. data/docs/resolvers.md +338 -25
  15. data/docs/security.md +227 -0
  16. data/docs/subscriptions.md +60 -37
  17. data/docs/types.md +219 -0
  18. data/docs/uploads.md +80 -0
  19. data/lib/generators/lighthouse/base_classes.rb +70 -0
  20. data/lib/generators/lighthouse/class_generator.rb +19 -8
  21. data/lib/generators/lighthouse/install/install_generator.rb +96 -20
  22. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +19 -10
  23. data/lib/generators/lighthouse/install/templates/schema.rb.tt +21 -2
  24. data/lib/generators/lighthouse/mutation_generator.rb +5 -4
  25. data/lib/generators/lighthouse/query_generator.rb +5 -4
  26. data/lib/generators/lighthouse/subscription_generator.rb +21 -16
  27. data/lib/lighthouse/async_mutation_job.rb +17 -0
  28. data/lib/lighthouse/complexity.rb +175 -0
  29. data/lib/lighthouse/complexity_analyzer.rb +56 -0
  30. data/lib/lighthouse/config.rb +16 -4
  31. data/lib/lighthouse/controller.rb +27 -9
  32. data/lib/lighthouse/dataloader_sources/preload_loader.rb +16 -0
  33. data/lib/lighthouse/dataloader_sources/relation_counts_loader.rb +46 -0
  34. data/lib/lighthouse/default_resolve_dispatcher.rb +26 -0
  35. data/lib/lighthouse/directive_resolvers/all_resolver.rb +1 -1
  36. data/lib/lighthouse/directive_resolvers/argument_binding.rb +61 -0
  37. data/lib/lighthouse/directive_resolvers/argument_transforms_resolver.rb +220 -0
  38. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +2 -2
  39. data/lib/lighthouse/directive_resolvers/async_resolver.rb +90 -0
  40. data/lib/lighthouse/directive_resolvers/base.rb +63 -14
  41. data/lib/lighthouse/directive_resolvers/builder_applier.rb +89 -19
  42. data/lib/lighthouse/directive_resolvers/can_resolvers.rb +1 -1
  43. data/lib/lighthouse/directive_resolvers/convention_resolver.rb +16 -5
  44. data/lib/lighthouse/directive_resolvers/convert_empty_strings_resolver.rb +1 -1
  45. data/lib/lighthouse/directive_resolvers/count_resolver.rb +1 -1
  46. data/lib/lighthouse/directive_resolvers/field_resolver.rb +35 -7
  47. data/lib/lighthouse/directive_resolvers/first_resolver.rb +1 -1
  48. data/lib/lighthouse/directive_resolvers/global_id_resolver.rb +57 -0
  49. data/lib/lighthouse/directive_resolvers/lazy_load_resolver.rb +61 -0
  50. data/lib/lighthouse/directive_resolvers/limit_resolver.rb +59 -0
  51. data/lib/lighthouse/directive_resolvers/method_resolver.rb +38 -3
  52. data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +1 -1
  53. data/lib/lighthouse/directive_resolvers/namespaced_resolver.rb +31 -0
  54. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +1 -1
  55. data/lib/lighthouse/directive_resolvers/resolution.rb +19 -4
  56. data/lib/lighthouse/directive_resolvers/subscription_resolver.rb +14 -5
  57. data/lib/lighthouse/directive_resolvers/throttle_resolver.rb +77 -0
  58. data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +81 -0
  59. data/lib/lighthouse/directive_resolvers/with_count_resolver.rb +43 -0
  60. data/lib/lighthouse/directive_resolvers/with_resolver.rb +42 -0
  61. data/lib/lighthouse/directives/arguments/more_filters.rb +215 -0
  62. data/lib/lighthouse/directives/definitions.rb +52 -0
  63. data/lib/lighthouse/directives/model_directive.rb +131 -0
  64. data/lib/lighthouse/directives/node_directive.rb +134 -0
  65. data/lib/lighthouse/directives/pagination_manipulator.rb +1 -1
  66. data/lib/lighthouse/directives/registry.rb +21 -20
  67. data/lib/lighthouse/directives/visibility_directive.rb +71 -0
  68. data/lib/lighthouse/enum_directive.rb +41 -0
  69. data/lib/lighthouse/events/execution_trace.rb +2 -1
  70. data/lib/lighthouse/events/lifecycle.rb +3 -1
  71. data/lib/lighthouse/field_resolver_stack.rb +59 -11
  72. data/lib/lighthouse/generated_resolvers.rb +13 -8
  73. data/lib/lighthouse/global_id.rb +31 -0
  74. data/lib/lighthouse/graphql/version.rb +1 -1
  75. data/lib/lighthouse/multipart_request.rb +43 -0
  76. data/lib/lighthouse/node_registry.rb +68 -0
  77. data/lib/lighthouse/pagination.rb +3 -0
  78. data/lib/lighthouse/rb_lighthouse.rb +36 -0
  79. data/lib/lighthouse/relation_counts.rb +29 -0
  80. data/lib/lighthouse/resolve_info.rb +80 -0
  81. data/lib/lighthouse/resolver_bases.rb +68 -0
  82. data/lib/lighthouse/resolver_dispatch.rb +36 -11
  83. data/lib/lighthouse/scalar_coercion.rb +72 -0
  84. data/lib/lighthouse/schema_cache.rb +2 -1
  85. data/lib/lighthouse/schema_factory.rb +0 -1
  86. data/lib/lighthouse/schema_generator.rb +21 -12
  87. data/lib/lighthouse/schema_implementation.rb +4 -8
  88. data/lib/lighthouse/sdl_loader.rb +0 -2
  89. data/lib/lighthouse/support/environment.rb +13 -0
  90. data/lib/lighthouse/support/model_resolver.rb +8 -6
  91. data/lib/lighthouse/support/preload.rb +49 -0
  92. data/lib/lighthouse/type.rb +103 -0
  93. data/lib/lighthouse/type_resolution.rb +150 -0
  94. data/lib/lighthouse/upload.rb +26 -0
  95. data/lib/lighthouse/upload_store.rb +32 -0
  96. data/sig/lighthouse/directives.rbs +3 -2
  97. data/sig/lighthouse/graphql.rbs +29 -0
  98. data/sig/lighthouse.rbs +18 -0
  99. metadata +43 -1
@@ -24,32 +24,24 @@ a hard dependency of Lighthouse so non-federated apps stay lean.
24
24
  bin/rails generate lighthouse:install
25
25
  ```
26
26
 
27
- does steps 1–5 below for you:
27
+ does steps 1–5 below for you. It runs graphql-ruby's `graphql:install` first, for
28
+ the base classes every graphql-ruby app has (`Types::Base*`, `Resolvers::BaseResolver`,
29
+ `Mutations::BaseMutation`), then puts Lighthouse's own on top:
28
30
 
29
31
  | File | What it is |
30
32
  | --- | --- |
31
33
  | `config/initializers/lighthouse.rb` | Every [configuration](configuration.md) option, commented out at its default |
32
34
  | `graphql/schema.graphql` | A starter schema (`user` via `@find`, `users` via `@paginate`) that assumes a `User` model — skipped when the folder already holds `.graphql` files |
33
- | `app/graphql/app_schema.rb` | The executable schema, with `GraphQL::Dataloader` enabled |
35
+ | `app/graphql/app_schema.rb` | The executable schema, built from the SDL, with `GraphQL::Dataloader` enabled and the base classes in `base_types` |
36
+ | `app/graphql/{types,resolvers,mutations,subscriptions}/base_*.rb` | graphql-ruby's base classes, and Lighthouse's `Subscriptions::BaseSubscription` |
34
37
  | `app/controllers/graphql_controller.rb` | The endpoint, plus a `post '/graphql'` route |
35
38
  | `graphql.config.yml` | IDE configuration — see [IDE support](#ide-support) |
36
39
 
37
- | Option | Effect |
38
- | --- | --- |
39
- | `--schema=ApiSchema` | Name of the schema constant (default `AppSchema`) |
40
- | `--sdl-folder=app/schema` | Where the `.graphql` files live (default `graphql`); sets `config.sdl_folder` |
41
- | `--skip-controller` | No controller or route — for apps that already serve GraphQL |
42
- | `--skip-ide` | No `graphql.config.yml` or `.gitignore` entries |
43
-
44
- Rerunning it is safe: the route and `.gitignore` entries are only added once, and
45
- an existing schema class or controller is kept (pass `--force` to replace it).
46
-
47
- It coexists with graphql-ruby's own tooling. The generators live under
48
- `lighthouse:` and the rake task is `lighthouse:ide_helper`, next to graphql-ruby's
49
- `graphql:*` generators and `graphql:schema:*` tasks. `graphql:install` writes the
50
- same controller and route; whichever runs second finds the route already in place,
51
- and Lighthouse keeps a controller graphql-ruby generated. In that case, have the
52
- controller call `AppSchema.execute(...)` to serve the Lighthouse schema.
40
+ `--schema=ApiSchema`, `--sdl-folder=app/schema`, `--skip-controller` and
41
+ `--skip-ide` change what it writes. Rerunning it is safe, and a schema class or
42
+ controller the app had before is kept (pass `--force` to replace it).
43
+ [generators.md](generators.md) has the details, every generator Lighthouse has, and
44
+ which of graphql-ruby's `graphql:*` generators fit a Lighthouse app.
53
45
 
54
46
  ## 1. Write your schema (SDL)
55
47
 
@@ -75,16 +67,48 @@ argument (the `@eq` makes `id` a filter). Lighthouse infers the model from the
75
67
  field's return type name (`User`), or you can be explicit with
76
68
  `@all(model: "User")`.
77
69
 
78
- ## 2. Provide your base types
70
+ ## 2. Provide your base classes
79
71
 
80
- Lighthouse builds on graphql-ruby, so you supply the base classes it should use
81
- for objects, interfaces, etc. — exactly the place to mix in Apollo Federation or
82
- any custom field/argument behavior.
72
+ Lighthouse builds on graphql-ruby, and follows its convention: each kind of class
73
+ inherits from a base class in its own namespace, where what they all share goes
74
+ once. graphql-ruby's `graphql:install` writes them, and `lighthouse:install` runs it;
75
+ by hand, run it yourself, then add the subscription base, which graphql-ruby has
76
+ none of, and make the mutation base a `GraphQL::Schema::Mutation` (the SDL declares a
77
+ mutation's arguments, where graphql-ruby's `RelayClassicMutation` takes one `input:`):
78
+
79
+ ```shell
80
+ bin/rails generate graphql:install --no-relay --skip-graphiql
81
+ ```
83
82
 
84
83
  ```ruby
85
- # app/graphql/base_object.rb
86
- class BaseObject < GraphQL::Schema::Object
87
- # include ApolloFederation::Object # if you use federation
84
+ # app/graphql/mutations/base_mutation.rb — Mutation fields (Mutations::PublishPost)
85
+ module Mutations
86
+ class BaseMutation < GraphQL::Schema::Mutation # graphql-ruby writes RelayClassicMutation
87
+ argument_class Types::BaseArgument
88
+ field_class Types::BaseField
89
+ object_class Types::BaseObject
90
+ end
91
+ end
92
+
93
+ # app/graphql/subscriptions/base_subscription.rb — Subscription fields
94
+ module Subscriptions
95
+ class BaseSubscription < GraphQL::Schema::Subscription
96
+ include Lighthouse::GraphQL::Subscription
97
+ end
98
+ end
99
+ ```
100
+
101
+ Delete the `Types::QueryType` and `Types::MutationType` it writes: the SDL declares
102
+ the root types. The `Types::Base*` classes are the SDL's types' bases: the place to
103
+ mix in Apollo Federation or custom field and argument behavior.
104
+
105
+ ```ruby
106
+ # app/graphql/types/base_object.rb
107
+ module Types
108
+ class BaseObject < GraphQL::Schema::Object
109
+ field_class Types::BaseField
110
+ # include ApolloFederation::Object # if you use federation
111
+ end
88
112
  end
89
113
  ```
90
114
 
@@ -98,13 +122,22 @@ AppSchema = Lighthouse::GraphQL::RbLightHouse.get_schema(
98
122
  sdl_folder: Rails.root.join('graphql').to_s,
99
123
  options: {
100
124
  base_types: {
101
- object: BaseObject,
125
+ resolver: Resolvers::BaseResolver,
126
+ mutation: Mutations::BaseMutation,
127
+ subscription: Subscriptions::BaseSubscription,
128
+ object: Types::BaseObject,
102
129
  # interface:, union:, enum:, input_object:, scalar: ...
103
130
  }
104
131
  }
105
132
  )
106
133
  ```
107
134
 
135
+ `object:` and the other SDL kinds go to graphql-ruby, which builds the SDL's types
136
+ from them. `resolver:`, `mutation:` and `subscription:` are Lighthouse's: every
137
+ class a `Query`, `Mutation` or `Subscription` field resolves to must inherit from
138
+ its kind's base, and a `Subscription` field with no class of its own resolves with
139
+ the subscription base. See [resolvers.md](resolvers.md#base-classes).
140
+
108
141
  `get_schema` returns a `GraphQL::Schema` subclass you can use directly, or
109
142
  subclass further (e.g. to add federation, tracing, a Dataloader):
110
143
 
@@ -247,18 +280,19 @@ type User {
247
280
  }
248
281
  ```
249
282
 
250
- ## Custom queries and mutations
283
+ ## Custom resolvers and mutations
251
284
 
252
285
  When a field needs Ruby, write a graphql-ruby resolver or mutation class named
253
- after it — `Query.latestPosts` resolves to `Queries::LatestPosts`
254
- (`app/graphql/queries/latest_posts.rb`), `Mutation.publishPost` to
255
- `Mutations::PublishPost`. `bin/rails generate lighthouse:query LatestPosts`
256
- scaffolds one. See [resolvers.md](resolvers.md).
286
+ after it — `Query.latestPosts` resolves to `Resolvers::LatestPosts`
287
+ (`app/graphql/resolvers/latest_posts.rb`, a `Resolvers::BaseResolver`),
288
+ `Mutation.publishPost` to `Mutations::PublishPost` (a `Mutations::BaseMutation`).
289
+ `bin/rails generate lighthouse:query LatestPosts` scaffolds one. See
290
+ [resolvers.md](resolvers.md).
257
291
 
258
292
  ## Next steps
259
293
 
260
294
  - [Directives reference](directives.md) — the full vocabulary.
261
- - [Queries, mutations & builders](resolvers.md), [Mutations](mutations.md),
295
+ - [Resolvers, mutations & builders](resolvers.md), [Mutations](mutations.md),
262
296
  [Validation](validation.md), [Caching](caching.md).
263
297
  - [Relationships](relationships.md) — wiring associations.
264
298
  - [Filtering & ordering](filtering-and-ordering.md) — query constraints.
data/docs/motivation.md CHANGED
@@ -151,8 +151,9 @@ Schema-first doesn't mean schema-only. When a field does something no directive
151
151
  describes, write Ruby, using graphql-ruby's own classes:
152
152
 
153
153
  - A `Query` or `Mutation` field without a resolving directive resolves by convention to
154
- `Queries::FieldName` (a `GraphQL::Schema::Resolver`) or `Mutations::FieldName` (a
155
- `GraphQL::Schema::Mutation`). See [Queries, Mutations and Builders](resolvers.md).
154
+ `Resolvers::FieldName` (a `Resolvers::BaseResolver`) or `Mutations::FieldName` (a
155
+ `Mutations::BaseMutation`), under the base classes graphql-ruby's convention gives
156
+ each kind. See [Resolvers, Mutations and Builders](resolvers.md).
156
157
  - `@builder` classes adjust a query; `@validator` contracts check arguments.
157
158
  - Your own directives plug in through the registry, with the same contracts as the
158
159
  built-in ones. See [Custom Directives](custom-directives.md).
@@ -31,6 +31,10 @@ 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
@@ -50,6 +54,44 @@ the Dataloader, the directives still work, but they load one parent at a time.
50
54
  A field with `scopes:` or a filter argument the client supplied runs its own query
51
55
  per parent, because each parent's query differs.
52
56
 
57
+ ## Eager loading for other fields
58
+
59
+ The relationship directives load their own association. Three directives load for
60
+ the fields that only read one: a method, a type class method, a field read off
61
+ the model. Each one loads across every parent in the response at once, through the
62
+ Dataloader:
63
+
64
+ - `@with(relation: "tasks")` loads a relation of the parent before the field
65
+ resolves. `user.tasks` is then in memory for the method that reads it.
66
+ - `@withCount(relation: "posts")` counts a relation for every parent in one
67
+ `GROUP BY` query, and keeps each count on its parent as `<relation>_count`, as
68
+ Laravel's `withCount` does. It is not the field's value: the field's resolver,
69
+ or the default reader of a `postsCount` field, reads `user.posts_count`.
70
+ - `@lazyLoad(relations: ["tags"])` loads relations of the records the field
71
+ returns: a list, a relation, a `@paginate` page. On a `@hasMany`, the records of
72
+ every parent load their relations together.
73
+
74
+ ```graphql
75
+ type User {
76
+ latestPostTitle: String @with(relation: "posts") @method
77
+ usage: String! @withCount(relation: "posts", scopes: ["published"]) @method
78
+ posts: [Post!]! @hasMany @lazyLoad(relations: ["tags"])
79
+ }
80
+ ```
81
+
82
+ ```ruby
83
+ class User < ApplicationRecord
84
+ def latest_post_title = posts.max_by(&:created_at)&.title
85
+ def usage = "#{posts_count} published posts"
86
+ end
87
+ ```
88
+
89
+ Relations may be nested with dots (`"posts.comments"`), and `scopes` narrow what is
90
+ loaded or counted, as model scopes. A count loads in one query when the database has
91
+ window functions and the relation links its parent through one column, as a
92
+ [paginated relation](#paginating-many-parents-at-once) does; otherwise once per
93
+ parent.
94
+
53
95
  ## Lists, and paginating them
54
96
 
55
97
  Collection directives return a **plain list**, as in PHP Lighthouse:
data/docs/relay.md ADDED
@@ -0,0 +1,137 @@
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
+ ## Global ids
10
+
11
+ `@globalId` on a field encodes its value with the type's name:
12
+
13
+ ```graphql
14
+ type User {
15
+ id: ID! @globalId
16
+ name: String
17
+ }
18
+ ```
19
+
20
+ `{ user(id: 1) { id } }` answers `"VXNlcjox"`, the base64 of `User:1`.
21
+
22
+ On an argument, or an input field, `@globalId` decodes what the client sent before
23
+ the field resolves, so every resolver (a directive, `@field`, a `Resolvers::` class)
24
+ gets the decoded value. `decode:` says which part:
25
+
26
+ | `decode:` | The resolver gets |
27
+ | --- | --- |
28
+ | `ARRAY` (default) | `["User", "1"]` |
29
+ | `TYPE` | `"User"` |
30
+ | `ID` | `"1"` |
31
+
32
+ ```graphql
33
+ type Query {
34
+ user(id: ID! @globalId(decode: ID) @eq): User @find
35
+ }
36
+
37
+ type Mutation {
38
+ deleteNode(id: ID! @globalId): Node
39
+ }
40
+ ```
41
+
42
+ Lists of ids decode item by item. A value that is not a global id makes the field
43
+ fail with `"…" is not a valid global id.`
44
+
45
+ ## Nodes
46
+
47
+ `@node` registers a type for refetching:
48
+
49
+ ```graphql
50
+ type User @node {
51
+ id: ID! @globalId
52
+ name: String
53
+ }
54
+ ```
55
+
56
+ and adds what Relay expects, leaving alone whatever the SDL declares itself:
57
+
58
+ ```graphql
59
+ "Any object implementing this type can be found by ID through `Query.node`."
60
+ interface Node {
61
+ "Global identifier that can be used to resolve any Node implementation."
62
+ id: ID!
63
+ }
64
+
65
+ type User implements Node @node { ... }
66
+
67
+ type Query {
68
+ "Fetches an object given its globally unique `ID`."
69
+ node(id: ID! @globalId): Node
70
+ }
71
+ ```
72
+
73
+ ```graphql
74
+ {
75
+ node(id: "VXNlcjox") {
76
+ id
77
+ ... on User { name }
78
+ }
79
+ }
80
+ ```
81
+
82
+ The type keeps its own `id: ID! @globalId`, which encodes the ids `node` decodes.
83
+ `node` answers `null` for an id with no object, and an error for the id of a type
84
+ without `@node`.
85
+
86
+ ### Where the object comes from
87
+
88
+ 1. `@node(resolver: "Countries@by_code")` names a class, and a method after the `@`
89
+ (`call` without one). It gets the decoded id and the context, and returns any
90
+ object, a Hash included:
91
+
92
+ ```graphql
93
+ type Country @node(resolver: "Countries@by_code") {
94
+ id: ID! @globalId
95
+ name: String!
96
+ }
97
+ ```
98
+
99
+ ```ruby
100
+ class Resolvers::Countries
101
+ COUNTRIES = { 'DE' => { id: 'DE', name: 'Germany' } }.freeze
102
+
103
+ def self.by_code(id, _context) = COUNTRIES[id]
104
+ end
105
+ ```
106
+
107
+ The class is looked up as `@field` looks one up on a type: in the resolvers
108
+ namespace, then in [`resolver_namespaces`](configuration.md#resolver_namespaces).
109
+ 2. `@node(model: "Account")` names the model.
110
+ 3. Otherwise, the type's [`@model`](directives.md#model), or the model named
111
+ after the type.
112
+
113
+ A model's record is found by its primary key, through graphql-ruby's Dataloader, so
114
+ several `node` fields in one query load together.
115
+
116
+ To give `Query.node` your own description or middleware, declare it, with the
117
+ registry Lighthouse uses:
118
+
119
+ ```graphql
120
+ type Query {
121
+ "This description is up to you."
122
+ node(id: ID! @globalId): Node
123
+ @field(resolver: "Lighthouse::GraphQL::NodeRegistry@resolve")
124
+ @guard
125
+ }
126
+ ```
127
+
128
+ ## Your own encoding
129
+
130
+ [`global_id`](configuration.md#global_id) takes any object with
131
+ `encode(type, id)` and `decode(global_id)`, which returns `[type, id]`:
132
+
133
+ ```ruby
134
+ Lighthouse.configure do |config|
135
+ config.global_id = MyGlobalIds # e.g. signed, or a UUID lookup
136
+ end
137
+ ```