lighthouse-graphql 0.1.1 → 0.2.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 +154 -1
- data/README.md +42 -2
- data/docs/authorization.md +116 -57
- data/docs/best-practices.md +16 -3
- data/docs/caching.md +161 -0
- data/docs/configuration.md +83 -4
- data/docs/custom-directives.md +5 -1
- data/docs/directives.md +117 -38
- data/docs/getting-started.md +84 -0
- data/docs/mutations.md +260 -0
- data/docs/performance.md +138 -0
- data/docs/relationships.md +80 -33
- data/docs/resolvers.md +179 -0
- data/docs/validation.md +132 -0
- data/lib/generators/lighthouse/builder_generator.rb +30 -0
- data/lib/generators/lighthouse/class_generator.rb +61 -0
- data/lib/generators/lighthouse/install/install_generator.rb +127 -0
- data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +8 -0
- data/lib/generators/lighthouse/install/templates/graphql_controller.rb.tt +57 -0
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +67 -0
- data/lib/generators/lighthouse/install/templates/schema.graphql.tt +19 -0
- data/lib/generators/lighthouse/install/templates/schema.rb.tt +10 -0
- data/lib/generators/lighthouse/mutation_generator.rb +30 -0
- data/lib/generators/lighthouse/query_generator.rb +29 -0
- data/lib/generators/lighthouse/validator_generator.rb +36 -0
- data/lib/lighthouse/cache_control.rb +126 -0
- data/lib/lighthouse/caching.rb +68 -0
- data/lib/lighthouse/config.rb +85 -19
- data/lib/lighthouse/contracts/arg_builder.rb +8 -12
- data/lib/lighthouse/contracts/arg_manipulator.rb +8 -12
- data/lib/lighthouse/contracts/directive.rb +10 -14
- data/lib/lighthouse/contracts/field_manipulator.rb +8 -12
- data/lib/lighthouse/contracts/field_middleware.rb +7 -11
- data/lib/lighthouse/contracts/field_resolver.rb +9 -13
- data/lib/lighthouse/dataloader_sources/association_loader.rb +28 -19
- data/lib/lighthouse/dataloader_sources/model_loader.rb +10 -14
- data/lib/lighthouse/dataloader_sources/relation_count_loader.rb +16 -0
- data/lib/lighthouse/dataloader_sources/relation_page_loader.rb +18 -0
- data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +57 -69
- data/lib/lighthouse/directive_resolvers/all_resolver.rb +26 -57
- data/lib/lighthouse/directive_resolvers/association_resolvers.rb +161 -0
- data/lib/lighthouse/directive_resolvers/auth_resolver.rb +41 -38
- data/lib/lighthouse/directive_resolvers/base.rb +142 -86
- data/lib/lighthouse/directive_resolvers/builder_applier.rb +58 -56
- data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +133 -0
- data/lib/lighthouse/directive_resolvers/can_resolvers.rb +177 -0
- data/lib/lighthouse/directive_resolvers/convention_resolver.rb +70 -0
- data/lib/lighthouse/directive_resolvers/count_resolver.rb +42 -48
- data/lib/lighthouse/directive_resolvers/field_resolver.rb +27 -68
- data/lib/lighthouse/directive_resolvers/find_resolver.rb +42 -52
- data/lib/lighthouse/directive_resolvers/first_resolver.rb +39 -45
- data/lib/lighthouse/directive_resolvers/guard_resolver.rb +16 -22
- data/lib/lighthouse/directive_resolvers/method_resolver.rb +19 -25
- data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +219 -0
- data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +70 -128
- data/lib/lighthouse/directive_resolvers/rename_resolver.rb +18 -24
- data/lib/lighthouse/directive_resolvers/resolution.rb +31 -27
- data/lib/lighthouse/directive_resolvers/validator_resolver.rb +193 -0
- data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +164 -163
- data/lib/lighthouse/directives/arguments/base.rb +23 -31
- data/lib/lighthouse/directives/arguments/filters.rb +113 -93
- data/lib/lighthouse/directives/arguments/order_by.rb +41 -41
- data/lib/lighthouse/directives/pagination_info_types.rb +69 -0
- data/lib/lighthouse/directives/pagination_manipulator.rb +176 -0
- data/lib/lighthouse/directives/registry.rb +210 -141
- data/lib/lighthouse/directives/where_conditions_directive.rb +236 -200
- data/lib/lighthouse/errors.rb +27 -0
- data/lib/lighthouse/federation_directives.rb +53 -0
- data/lib/lighthouse/field_resolver_stack.rb +56 -0
- data/lib/lighthouse/generated_resolvers.rb +82 -0
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/lib/lighthouse/graphql_ruby_patches.rb +18 -0
- data/lib/lighthouse/ide_helper.rb +98 -0
- data/lib/lighthouse/namespaces.rb +21 -0
- data/lib/lighthouse/nested_mutations/belongs_to_operations.rb +50 -0
- data/lib/lighthouse/nested_mutations/executor.rb +149 -0
- data/lib/lighthouse/nested_mutations/has_operations.rb +66 -0
- data/lib/lighthouse/nested_mutations/many_to_many_operations.rb +44 -0
- data/lib/lighthouse/nested_mutations/partitioner.rb +125 -0
- data/lib/lighthouse/nested_mutations.rb +210 -0
- data/lib/lighthouse/pagination.rb +221 -0
- data/lib/lighthouse/persisted_queries.rb +167 -0
- data/lib/lighthouse/railtie.rb +26 -0
- data/lib/lighthouse/rb_lighthouse.rb +50 -29
- data/lib/lighthouse/reference_resolver.rb +13 -15
- data/lib/lighthouse/relation_batch.rb +156 -0
- data/lib/lighthouse/resolver_dispatch.rb +26 -0
- data/lib/lighthouse/schema_cache.rb +120 -0
- data/lib/lighthouse/schema_factory.rb +27 -37
- data/lib/lighthouse/schema_generator.rb +196 -239
- data/lib/lighthouse/schema_implementation.rb +51 -48
- data/lib/lighthouse/sdl_loader.rb +17 -20
- data/lib/lighthouse/support/arguments.rb +119 -0
- data/lib/lighthouse/support/authorization.rb +18 -19
- data/lib/lighthouse/support/batching.rb +16 -0
- data/lib/lighthouse/support/directive_args.rb +26 -24
- data/lib/lighthouse/support/model_resolver.rb +56 -56
- data/lib/lighthouse/support/naming.rb +55 -59
- data/lib/lighthouse/tasks/lighthouse.rake +33 -0
- data/lib/lighthouse/where_conditions/operator_map.rb +25 -29
- metadata +51 -63
- data/lib/lighthouse/directive_resolvers/belongs_to_many_resolver.rb +0 -56
- data/lib/lighthouse/directive_resolvers/belongs_to_resolver.rb +0 -47
- data/lib/lighthouse/directive_resolvers/can_resolver.rb +0 -86
- data/lib/lighthouse/directive_resolvers/has_many_resolver.rb +0 -109
- data/lib/lighthouse/directive_resolvers/has_one_resolver.rb +0 -43
- data/lib/lighthouse/directives/relation_directive.rb +0 -332
data/docs/caching.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Caching
|
|
2
|
+
|
|
3
|
+
Two independent layers, matching PHP Lighthouse:
|
|
4
|
+
|
|
5
|
+
- **Application cache.** `@cache` stores a field's resolved value in the Rails
|
|
6
|
+
cache (`Rails.cache`), so later requests skip the resolver.
|
|
7
|
+
- **HTTP cache.** `@cacheControl` computes the `Cache-Control` header a response
|
|
8
|
+
may carry, so browsers, CDNs and proxies can reuse it.
|
|
9
|
+
|
|
10
|
+
## `@cache`
|
|
11
|
+
|
|
12
|
+
```graphql
|
|
13
|
+
directive @cache(maxAge: Int, private: Boolean = false) on FIELD_DEFINITION
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```graphql
|
|
17
|
+
type Query {
|
|
18
|
+
highestKnownPrime: Int! @field(resolver: "Primes") @cache
|
|
19
|
+
temperature: Int! @cache(maxAge: 300)
|
|
20
|
+
myTodos: [Todo!]! @cache(private: true)
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- **`maxAge`** is in seconds. Without it, an entry lives until `@clearCache`
|
|
25
|
+
invalidates it or the store evicts it.
|
|
26
|
+
- **`private: true`** keeps one entry per authenticated user (by `id`). Guests
|
|
27
|
+
share one.
|
|
28
|
+
- **The key** covers the parent type, the parent's key, the field and its
|
|
29
|
+
arguments. The parent's key is the field marked `@cacheKey`, else its first
|
|
30
|
+
`ID` field; root types have none.
|
|
31
|
+
|
|
32
|
+
```graphql
|
|
33
|
+
type GithubProfile {
|
|
34
|
+
username: String @cacheKey
|
|
35
|
+
repos: [Repository!]! @cache # one entry per username
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Lists and relations are stored as arrays. A value the store cannot serialize,
|
|
40
|
+
such as a Relay connection object, is reported through
|
|
41
|
+
[`on_error`](configuration.md#on_error) and returned uncached. So cache list
|
|
42
|
+
fields, not connection fields.
|
|
43
|
+
|
|
44
|
+
The store is [`Config.cache_store`](configuration.md#cache_store), `Rails.cache`
|
|
45
|
+
by default. Note that Rails uses a `:null_store` in development and test unless
|
|
46
|
+
you enable caching (`bin/rails dev:cache`).
|
|
47
|
+
|
|
48
|
+
A cached value is still authorized and validated on every request: `@cache` runs
|
|
49
|
+
inside `@guard`, `@can*` and `@validator`, never around them.
|
|
50
|
+
|
|
51
|
+
## `@clearCache`
|
|
52
|
+
|
|
53
|
+
```graphql
|
|
54
|
+
directive @clearCache(type: String!, idSource: ClearCacheIdSource, field: String) repeatable on FIELD_DEFINITION
|
|
55
|
+
|
|
56
|
+
input ClearCacheIdSource {
|
|
57
|
+
argument: String # path of an argument, e.g. "input.id"
|
|
58
|
+
field: String # path in the result, e.g. "*.id"
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Put it on a mutation. It invalidates entries once the mutation has resolved
|
|
63
|
+
successfully; a failed mutation clears nothing.
|
|
64
|
+
|
|
65
|
+
```graphql
|
|
66
|
+
type Mutation {
|
|
67
|
+
# Every cached field of every Post.
|
|
68
|
+
importPosts: Boolean @clearCache(type: "Post")
|
|
69
|
+
|
|
70
|
+
# The cached fields of the post whose id the argument holds.
|
|
71
|
+
updatePost(input: UpdatePostInput! @spread): Post! @update
|
|
72
|
+
@clearCache(type: "Post", idSource: { argument: "input.id" })
|
|
73
|
+
|
|
74
|
+
# One field of each post the mutation returned.
|
|
75
|
+
retitlePosts(ids: [ID!]!, title: String!): [Post!]!
|
|
76
|
+
@clearCache(type: "Post", idSource: { field: "*.id" }, field: "title")
|
|
77
|
+
|
|
78
|
+
# A root field.
|
|
79
|
+
publishPost(id: ID!): Post @clearCache(type: "Query", field: "latestPosts")
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### How invalidation works
|
|
84
|
+
|
|
85
|
+
PHP Lighthouse clears entries through cache tags. Rails cache stores have no tags,
|
|
86
|
+
and `delete_matched` is not available everywhere (Solid Cache and Memcached lack
|
|
87
|
+
it). So Lighthouse **versions** keys instead:
|
|
88
|
+
|
|
89
|
+
1. Each entry's key embeds four version tokens: the type, the type and id, the
|
|
90
|
+
type and field, and the type, id and field.
|
|
91
|
+
2. `@clearCache` replaces the token it targets, so the entries built on the old
|
|
92
|
+
one are never read again. They expire or are evicted on their own.
|
|
93
|
+
3. If a token is evicted, a new one is written, which causes misses, never stale
|
|
94
|
+
reads.
|
|
95
|
+
|
|
96
|
+
This works on any store, and clearing a type also clears its per-id entries.
|
|
97
|
+
Each cached read costs one `read_multi` for the tokens.
|
|
98
|
+
|
|
99
|
+
## `@cacheControl`
|
|
100
|
+
|
|
101
|
+
```graphql
|
|
102
|
+
directive @cacheControl(maxAge: Int! = 0, scope: CacheControlScope! = PUBLIC, inheritMaxAge: Boolean! = false)
|
|
103
|
+
on FIELD_DEFINITION | OBJECT | INTERFACE | UNION
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Lighthouse walks the fields a query selects, fragments included, and derives one
|
|
107
|
+
header. These are PHP Lighthouse's rules, after Apollo's:
|
|
108
|
+
|
|
109
|
+
- `max-age` is the **lowest** `maxAge` among the contributing fields. 0 gives
|
|
110
|
+
`no-cache`.
|
|
111
|
+
- The response is `private` if any selected field is `PRIVATE`.
|
|
112
|
+
- **Defaults:**
|
|
113
|
+
- A root field without the directive is `maxAge: 0, PRIVATE`.
|
|
114
|
+
- Any other field returning an object, interface or union is 0, unless its
|
|
115
|
+
type carries `@cacheControl`.
|
|
116
|
+
- Scalar fields inherit their parent and don't contribute.
|
|
117
|
+
- A directive on the field overrides one on its type, and its omitted arguments
|
|
118
|
+
take the directive's defaults (0, `PUBLIC`).
|
|
119
|
+
- `inheritMaxAge: true` takes the parent field's `maxAge`.
|
|
120
|
+
- Mutations are never cacheable.
|
|
121
|
+
|
|
122
|
+
```graphql
|
|
123
|
+
type Query {
|
|
124
|
+
publicCompanies: [Company!]! @cacheControl(maxAge: 15)
|
|
125
|
+
me: User! @guard @cacheControl(maxAge: 5, scope: PRIVATE)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
type Company @cacheControl(maxAge: 40) {
|
|
129
|
+
users: [User!]! @hasMany @cacheControl(maxAge: 25)
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`{ publicCompanies { users { id } } }` gets `max-age=15, public`;
|
|
134
|
+
`{ me { id } }` gets `max-age=5, private`.
|
|
135
|
+
|
|
136
|
+
### Sending the header
|
|
137
|
+
|
|
138
|
+
The controller from `rails generate lighthouse:install` already does this:
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
result = AppSchema.execute(params[:query], ...)
|
|
142
|
+
Lighthouse::GraphQL::CacheControl.apply(response, result) # no-op without @cacheControl
|
|
143
|
+
render json: result
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`Lighthouse::GraphQL::CacheControl.header(result)` returns the value without
|
|
147
|
+
setting it.
|
|
148
|
+
|
|
149
|
+
Things to know:
|
|
150
|
+
|
|
151
|
+
- Shared caches only store `GET` responses, and the generated route is
|
|
152
|
+
`POST /graphql`. Add `get '/graphql', to: 'graphql#execute'` and send cacheable
|
|
153
|
+
queries over `GET`, ideally as [persisted queries](performance.md#persisted-queries),
|
|
154
|
+
whose hash fits in a URL.
|
|
155
|
+
- The pagination types Lighthouse generates (`paginatorInfo`, `data`, `pageInfo`,
|
|
156
|
+
`edges`, `node`) inherit their parent field's `maxAge`, as in PHP Lighthouse,
|
|
157
|
+
so a paginated field is as cacheable as its `@cacheControl` says.
|
|
158
|
+
- With a `public` max-age, Rack's ETag middleware adds an `ETag`, so clients can
|
|
159
|
+
revalidate with `If-None-Match`.
|
|
160
|
+
- Rails normalizes `no-cache, private` to `no-cache`. `no-cache` already forces
|
|
161
|
+
every cache to revalidate, so nothing is stored stale.
|
data/docs/configuration.md
CHANGED
|
@@ -10,6 +10,7 @@ require 'lighthouse-graphql'
|
|
|
10
10
|
|
|
11
11
|
Lighthouse.configure do |config|
|
|
12
12
|
config.sdl_folder = Rails.root.join('graphql').to_s
|
|
13
|
+
config.namespaces = { queries: ['Queries'], mutations: ['Mutations'] }
|
|
13
14
|
config.resolver_namespaces = ['App::Graphql', 'Resolvers']
|
|
14
15
|
config.on_error = ->(error, _context) { Rails.error.report(error) }
|
|
15
16
|
end
|
|
@@ -23,12 +24,28 @@ Where Lighthouse loads `*.graphql` files from. Defaults to `RAILS_ROOT/graphql`
|
|
|
23
24
|
in a Rails app, `nil` otherwise (pass SDL explicitly via `SchemaFactory` when not
|
|
24
25
|
on Rails).
|
|
25
26
|
|
|
27
|
+
### `namespaces`
|
|
28
|
+
|
|
29
|
+
Where each kind of class a schema names is looked up, by kind — they have
|
|
30
|
+
different contracts, so they live apart ([resolvers.md](resolvers.md)):
|
|
31
|
+
|
|
32
|
+
| Kind | Default | Holds |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| `queries` | `['Queries']` | `GraphQL::Schema::Resolver` classes for `Query` fields |
|
|
35
|
+
| `mutations` | `['Mutations']` | `GraphQL::Schema::Mutation` classes for `Mutation` fields |
|
|
36
|
+
| `builders` | `['Builders']` | `@builder` classes |
|
|
37
|
+
| `validators` | `['Validators']` | `@validator` contracts |
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
config.namespaces = { queries: ['Api::Queries'] } # the other kinds keep their defaults
|
|
41
|
+
```
|
|
42
|
+
|
|
26
43
|
### `resolver_namespaces`
|
|
27
44
|
|
|
28
|
-
An ordered list of namespaces tried
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
45
|
+
An ordered list of namespaces tried for **models** named by a directive
|
|
46
|
+
(`@all(model: "User")`) and for `@field(resolver:)` on types other than `Query`
|
|
47
|
+
and `Mutation`. Fully-qualified names (containing `::`) are used as-is. Defaults
|
|
48
|
+
to `['App::Graphql']`.
|
|
32
49
|
|
|
33
50
|
```ruby
|
|
34
51
|
config.resolver_namespaces = ['App::Graphql', 'Resolvers']
|
|
@@ -48,6 +65,52 @@ reporter in production:
|
|
|
48
65
|
config.on_error = ->(error, _ctx) { Sentry.capture_exception(error) }
|
|
49
66
|
```
|
|
50
67
|
|
|
68
|
+
### `cache_store`
|
|
69
|
+
|
|
70
|
+
The store behind [`@cache` / `@clearCache`](caching.md). Defaults to
|
|
71
|
+
`Rails.cache` (an in-memory store outside Rails).
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
config.cache_store = ActiveSupport::Cache::RedisCacheStore.new(url: ENV['REDIS_URL'])
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### `pagination_default_count`, `pagination_max_count`
|
|
78
|
+
|
|
79
|
+
Defaults for paginated fields: the `first` used when a client leaves it out (so
|
|
80
|
+
it is no longer required), and the largest `first` allowed. A directive's
|
|
81
|
+
`defaultCount` / `maxCount` override them; `maxCount: null` lifts the cap.
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
config.pagination_default_count = 25
|
|
85
|
+
config.pagination_max_count = 100
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### `schema_cache`, `schema_cache_path`, `watch_sdl`, `persisted_queries*`
|
|
89
|
+
|
|
90
|
+
Options for large schemas and high traffic, all off by default. See
|
|
91
|
+
[performance.md](performance.md).
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
config.schema_cache = Rails.env.production? # cache the built schema in tmp/cache
|
|
95
|
+
config.watch_sdl = Rails.env.development? # reload when a .graphql file changes
|
|
96
|
+
config.persisted_queries = true # Apollo's Automatic Persisted Queries
|
|
97
|
+
config.persisted_queries_store = Rails.cache # where APQ keeps queries (default: cache_store)
|
|
98
|
+
config.persisted_queries_ttl = 1.week.to_i # how long they live there
|
|
99
|
+
config.persisted_queries_manifest = Rails.root.join('persisted-queries.json') # trusted operations
|
|
100
|
+
config.persisted_queries_only = Rails.env.production? # run nothing else
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### `transactional_mutations`
|
|
104
|
+
|
|
105
|
+
Whether `@create` / `@update` / `@createMany` / `@updateMany` run in a database
|
|
106
|
+
transaction. Defaults to `true`.
|
|
107
|
+
|
|
108
|
+
### `authenticated_user`, `policy_user`, `authorizer`
|
|
109
|
+
|
|
110
|
+
The authentication and authorization seams behind `@guard` and the `@can*`
|
|
111
|
+
family. The authorizer is `->(user, record, ability, args = [])`, Pundit by
|
|
112
|
+
default. See [authorization.md](authorization.md#configuration).
|
|
113
|
+
|
|
51
114
|
## Extending the registry
|
|
52
115
|
|
|
53
116
|
Register app-defined directives (see [custom-directives.md](custom-directives.md))
|
|
@@ -57,6 +120,22 @@ from the same initializer:
|
|
|
57
120
|
Lighthouse::DirectiveRegistry.register(App::Graphql::Directives::StartsWith)
|
|
58
121
|
```
|
|
59
122
|
|
|
123
|
+
Directives implemented inside another class — no class of their own to
|
|
124
|
+
register — contribute their SDL definition with `register_definition`, so it is
|
|
125
|
+
injected into the schema and written by `bin/rails lighthouse:ide_helper`:
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
Lighthouse::DirectiveRegistry.default.register_definition(
|
|
129
|
+
'tenant', 'directive @tenant(column: String) on FIELD_DEFINITION'
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Types a definition refers to (an enum argument, say) are registered with
|
|
134
|
+
`register_type_definition('TenantMode', 'enum TenantMode { STRICT LOOSE }')`, or
|
|
135
|
+
returned by the directive class's `type_definitions` (`{ name => sdl }`). They
|
|
136
|
+
are injected only alongside a directive that uses them, and only if the schema
|
|
137
|
+
doesn't define them itself.
|
|
138
|
+
|
|
60
139
|
## Federation reference resolvers
|
|
61
140
|
|
|
62
141
|
Custom federation entity resolvers are registered with
|
data/docs/custom-directives.md
CHANGED
|
@@ -40,8 +40,12 @@ def self.directive_name = 'upcase'
|
|
|
40
40
|
def self.definition = 'directive @upcase on FIELD_DEFINITION'
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
If the definition refers to types of its own (an enum argument, say), return
|
|
44
|
+
them from `def self.type_definitions = { 'Mode' => 'enum Mode { A B }' }`.
|
|
45
|
+
|
|
43
46
|
Lighthouse injects that `definition` into the schema automatically, so apps never
|
|
44
|
-
hand-declare your directive
|
|
47
|
+
hand-declare your directive, and `bin/rails lighthouse:ide_helper` writes it out
|
|
48
|
+
for the IDE (see [IDE support](getting-started.md#ide-support)).
|
|
45
49
|
|
|
46
50
|
## Example 1 — an argument filter
|
|
47
51
|
|
data/docs/directives.md
CHANGED
|
@@ -24,21 +24,27 @@ type Query {
|
|
|
24
24
|
}
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
The model defaults to the field's return type (`User`).
|
|
28
|
-
`@all(model: "Account")
|
|
29
|
-
|
|
27
|
+
It returns a plain list. The model defaults to the field's return type (`User`).
|
|
28
|
+
Override it with `@all(model: "Account")`, and narrow it with model scopes:
|
|
29
|
+
`@all(scopes: ["active"])`. Combine with
|
|
30
|
+
[argument filters](filtering-and-ordering.md) and [`@orderBy`](#orderby) to
|
|
31
|
+
constrain the result.
|
|
30
32
|
|
|
31
33
|
### `@find`
|
|
32
34
|
|
|
33
|
-
Return
|
|
34
|
-
|
|
35
|
+
Return the single record the field's argument filters select. More than one match
|
|
36
|
+
is an error (`The query returned more than one result.`); none returns `null`.
|
|
35
37
|
|
|
36
38
|
```graphql
|
|
37
39
|
type Query {
|
|
38
40
|
user(id: ID! @eq): User @find
|
|
41
|
+
userByEmail(email: String! @eq): User @find
|
|
39
42
|
}
|
|
40
43
|
```
|
|
41
44
|
|
|
45
|
+
A lookup by `id` alone is batched through the Dataloader. An `id` argument without
|
|
46
|
+
a filter directive still selects by primary key. `scopes:` applies model scopes.
|
|
47
|
+
|
|
42
48
|
### `@first`
|
|
43
49
|
|
|
44
50
|
Like `@all`, but returns the **first** matching record instead of a list. Useful
|
|
@@ -46,31 +52,43 @@ with filters to fetch one record by criteria.
|
|
|
46
52
|
|
|
47
53
|
```graphql
|
|
48
54
|
type Query {
|
|
49
|
-
latestPost(authorId: ID! @eq): Post @first
|
|
55
|
+
latestPost(authorId: ID! @eq): Post @first(scopes: ["published"])
|
|
50
56
|
}
|
|
51
57
|
```
|
|
52
58
|
|
|
53
59
|
### `@paginate`
|
|
54
60
|
|
|
55
|
-
Return a
|
|
56
|
-
|
|
61
|
+
Return one page of a model's records. Lighthouse rewrites the field and adds the
|
|
62
|
+
pagination arguments, exactly as PHP Lighthouse does:
|
|
63
|
+
|
|
64
|
+
| `type:` | Arguments | Returns |
|
|
65
|
+
| --- | --- | --- |
|
|
66
|
+
| `PAGINATOR` (default) | `first: Int!`, `page: Int` | `PostPaginator! { paginatorInfo: PaginatorInfo!, data: [Post!]! }` |
|
|
67
|
+
| `SIMPLE` | `first: Int!`, `page: Int` | `PostSimplePaginator! { paginatorInfo: SimplePaginatorInfo!, data }`, with no total, so one query fewer |
|
|
68
|
+
| `CONNECTION` | `first: Int!`, `after: String` | `PostConnection! { pageInfo: PageInfo!, edges: [PostEdge!]! }`, Relay style |
|
|
57
69
|
|
|
58
70
|
```graphql
|
|
59
71
|
type Query {
|
|
60
|
-
posts: [Post!]! @paginate
|
|
72
|
+
posts(title: String @like): [Post!]! @paginate(defaultCount: 20, maxCount: 100)
|
|
61
73
|
}
|
|
62
74
|
```
|
|
63
75
|
|
|
64
|
-
becomes, in effect:
|
|
65
|
-
|
|
66
76
|
```graphql
|
|
67
|
-
|
|
68
|
-
posts(first: Int, after: String): PostConnection
|
|
69
|
-
}
|
|
77
|
+
{ posts(first: 10, page: 2) { paginatorInfo { total lastPage hasMorePages } data { title } } }
|
|
70
78
|
```
|
|
71
79
|
|
|
72
|
-
|
|
73
|
-
|
|
80
|
+
- **The query.** It is the model's (`model:`, else the return type), narrowed by
|
|
81
|
+
`scopes:`, [`@builder`](#builder) and the field's argument filters. On a nested
|
|
82
|
+
field it pages the parent's association of the same name: `User.posts @paginate`
|
|
83
|
+
pages the user's posts.
|
|
84
|
+
- **`first`.** It defaults to `defaultCount`, else
|
|
85
|
+
[`Config.pagination_default_count`](configuration.md#pagination_default_count--pagination_max_count).
|
|
86
|
+
Without either, clients must pass it.
|
|
87
|
+
- **`maxCount`.** It caps `first`, overriding `Config.pagination_max_count`;
|
|
88
|
+
`maxCount: null` means unlimited.
|
|
89
|
+
- **The total** is only counted when the client selects `paginatorInfo` or
|
|
90
|
+
`pageInfo`.
|
|
91
|
+
- **Cursors** are base64 of the item's position.
|
|
74
92
|
|
|
75
93
|
### `@count`
|
|
76
94
|
|
|
@@ -79,6 +97,7 @@ Return the number of records — of a model, or of a relation on the parent obje
|
|
|
79
97
|
```graphql
|
|
80
98
|
type Query {
|
|
81
99
|
userCount: Int! @count(model: "User")
|
|
100
|
+
activeUserCount: Int! @count(model: "User", scopes: ["active"])
|
|
82
101
|
}
|
|
83
102
|
|
|
84
103
|
type Account {
|
|
@@ -92,7 +111,7 @@ Compute an aggregate (`SUM`, `AVG`, `MIN`, `MAX`, `COUNT`) over a column.
|
|
|
92
111
|
|
|
93
112
|
```graphql
|
|
94
113
|
type Account {
|
|
95
|
-
totalRevenue: Float @aggregate(relation: "orders", column: "amount", function: SUM)
|
|
114
|
+
totalRevenue: Float @aggregate(relation: "orders", column: "amount", function: "SUM")
|
|
96
115
|
}
|
|
97
116
|
```
|
|
98
117
|
|
|
@@ -105,10 +124,11 @@ See [relationships.md](relationships.md) for the full guide.
|
|
|
105
124
|
|
|
106
125
|
| Directive | Association |
|
|
107
126
|
| --- | --- |
|
|
108
|
-
| `@hasMany` | one-to-many
|
|
127
|
+
| `@hasMany` | one-to-many: a list, or paginated with `type:` |
|
|
109
128
|
| `@hasOne` | one-to-one |
|
|
110
129
|
| `@belongsTo` | inverse of has-one/has-many |
|
|
111
|
-
| `@belongsToMany` | many-to-many |
|
|
130
|
+
| `@belongsToMany` | many-to-many: a list, or paginated with `type:` |
|
|
131
|
+
| `@morphTo`, `@morphOne`, `@morphMany`, `@morphToMany` | the polymorphic variants |
|
|
112
132
|
|
|
113
133
|
```graphql
|
|
114
134
|
type User {
|
|
@@ -128,8 +148,11 @@ The relation name defaults to the field name (snake_cased). Override with
|
|
|
128
148
|
|
|
129
149
|
### `@field`
|
|
130
150
|
|
|
131
|
-
Delegate a field to a
|
|
132
|
-
|
|
151
|
+
Delegate a field to a class — your escape hatch for anything the built-in
|
|
152
|
+
directives don't cover. On `Query` and `Mutation` you usually don't need it:
|
|
153
|
+
a field without a resolving directive resolves to `Queries::<FieldName>` /
|
|
154
|
+
`Mutations::<FieldName>`, graphql-ruby resolver and mutation classes. See
|
|
155
|
+
[resolvers.md](resolvers.md).
|
|
133
156
|
|
|
134
157
|
```graphql
|
|
135
158
|
type Contact {
|
|
@@ -137,10 +160,11 @@ type Contact {
|
|
|
137
160
|
}
|
|
138
161
|
```
|
|
139
162
|
|
|
140
|
-
|
|
141
|
-
[`resolver_namespaces`](configuration.md).
|
|
142
|
-
`
|
|
143
|
-
`
|
|
163
|
+
On `Query` and `Mutation` the name is looked up in the queries / mutations
|
|
164
|
+
namespaces, elsewhere in [`resolver_namespaces`](configuration.md). The class may
|
|
165
|
+
be a `GraphQL::Schema::Resolver` (graphql-ruby's contract), or a plain callable —
|
|
166
|
+
a class-level `call`, an instance `#call`, or an instance `#resolve`, each
|
|
167
|
+
receiving `(object, args, context)`:
|
|
144
168
|
|
|
145
169
|
```ruby
|
|
146
170
|
module Resolvers
|
|
@@ -189,25 +213,27 @@ Require an authenticated user before the field resolves; otherwise raise
|
|
|
189
213
|
|
|
190
214
|
```graphql
|
|
191
215
|
type Query {
|
|
192
|
-
me: User @
|
|
216
|
+
me: User @guard
|
|
193
217
|
}
|
|
194
218
|
```
|
|
195
219
|
|
|
196
|
-
### `@
|
|
220
|
+
### `@canFind`, `@canModel`, `@canQuery`, `@canResolved`, `@canRoot`
|
|
197
221
|
|
|
198
|
-
Authorize a field with a policy ability (Pundit by default; configurable)
|
|
199
|
-
|
|
222
|
+
Authorize a field with a policy ability (Pundit by default; configurable) — each
|
|
223
|
+
directive authorizes something different: the models an argument identifies, the
|
|
224
|
+
model class, the models the field's filters select, the resolved value, or the
|
|
225
|
+
parent object. All `repeatable`; shared arguments `ability`, `injectArgs`, `args`,
|
|
226
|
+
`action`, `returnValue`.
|
|
200
227
|
|
|
201
228
|
```graphql
|
|
202
|
-
type
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
@can(ability: "show")
|
|
229
|
+
type Mutation {
|
|
230
|
+
createPost(input: CreatePostInput! @spread): Post @create @canModel(ability: "create")
|
|
231
|
+
updatePost(id: ID!, title: String): Post @update @canFind(ability: "update", find: "id")
|
|
206
232
|
}
|
|
207
233
|
```
|
|
208
234
|
|
|
209
|
-
|
|
210
|
-
|
|
235
|
+
See [authorization.md](authorization.md). (`@can` was removed in favour of this
|
|
236
|
+
family — it has a migration table.)
|
|
211
237
|
|
|
212
238
|
### `@auth`
|
|
213
239
|
|
|
@@ -231,13 +257,16 @@ Apply a custom method to the query builder before the field resolves. Repeatable
|
|
|
231
257
|
|
|
232
258
|
```graphql
|
|
233
259
|
type Query {
|
|
234
|
-
contacts: [Contact!]! @paginate @builder(class: "
|
|
260
|
+
contacts: [Contact!]! @paginate @builder(class: "Unassigned")
|
|
235
261
|
}
|
|
236
262
|
```
|
|
237
263
|
|
|
264
|
+
Builders are looked up in their own namespace, `Builders::` (app/graphql/builders)
|
|
265
|
+
— they are not resolvers. See [resolvers.md](resolvers.md#builders).
|
|
266
|
+
|
|
238
267
|
```ruby
|
|
239
|
-
module
|
|
240
|
-
class
|
|
268
|
+
module Builders
|
|
269
|
+
class Unassigned
|
|
241
270
|
def self.call(relation, args, _ctx)
|
|
242
271
|
args[:unassigned] ? relation.left_joins(:assignments).where(assignments: { id: nil }) : relation
|
|
243
272
|
end
|
|
@@ -260,6 +289,7 @@ Full guide: [filtering-and-ordering.md](filtering-and-ordering.md).
|
|
|
260
289
|
| `@in` | `column IN (values)` |
|
|
261
290
|
| `@notIn` | `column NOT IN (values)` |
|
|
262
291
|
| `@like` | `column LIKE %value%` |
|
|
292
|
+
| `@whereKey` | `primary key = value` (`IN` for a list) |
|
|
263
293
|
| `@where(operator:)` | generic operator (`GT`, `GTE`, `IN`, `IS_NULL`, `CONTAINS`, …) |
|
|
264
294
|
| `@orderBy` | sort by one or more columns |
|
|
265
295
|
|
|
@@ -290,6 +320,55 @@ type Query {
|
|
|
290
320
|
|
|
291
321
|
---
|
|
292
322
|
|
|
323
|
+
## Mutation directives
|
|
324
|
+
|
|
325
|
+
`@create`, `@createMany`, `@update`, `@updateMany`, `@upsert` and `@upsertMany`
|
|
326
|
+
save the model a field returns through ActiveRecord, with its validations, in a
|
|
327
|
+
transaction. Input on its associations runs as
|
|
328
|
+
[nested mutations](mutations.md#nested-mutations). `@delete` destroys the records
|
|
329
|
+
its filter arguments (`@whereKey`, `@eq`, …) select. `@spread` flattens an input
|
|
330
|
+
argument into the attributes, `@rename` maps an argument to another attribute,
|
|
331
|
+
and `@inject(context:, name:)` sets server-side attributes.
|
|
332
|
+
|
|
333
|
+
```graphql
|
|
334
|
+
type Mutation {
|
|
335
|
+
createPost(input: CreatePostInput! @spread): Post!
|
|
336
|
+
@create @inject(context: "current_user.id", name: "author_id")
|
|
337
|
+
updatePost(id: ID!, title: String): Post! @update
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
See [mutations.md](mutations.md).
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## Validation
|
|
346
|
+
|
|
347
|
+
`@validator` runs a contract (Dry::Validation by default) before a field
|
|
348
|
+
resolves — on a field, or on an input type wherever it is used. Model validations
|
|
349
|
+
need no directive. See [validation.md](validation.md).
|
|
350
|
+
|
|
351
|
+
```graphql
|
|
352
|
+
input CreateUserInput @validator { email: String password: String }
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## Caching
|
|
358
|
+
|
|
359
|
+
`@cache(maxAge:, private:)` keeps a field's value in `Rails.cache`, keyed with the
|
|
360
|
+
help of `@cacheKey`; `@clearCache(type:, idSource:, field:)` invalidates it from a
|
|
361
|
+
mutation. `@cacheControl(maxAge:, scope:, inheritMaxAge:)` computes the response's
|
|
362
|
+
HTTP `Cache-Control` header. See [caching.md](caching.md).
|
|
363
|
+
|
|
364
|
+
```graphql
|
|
365
|
+
type Query {
|
|
366
|
+
temperature: Int! @cache(maxAge: 300) @cacheControl(maxAge: 300)
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
293
372
|
## Adding your own
|
|
294
373
|
|
|
295
374
|
Every directive above is a small class registered with
|
data/docs/getting-started.md
CHANGED
|
@@ -18,6 +18,39 @@ Runtime dependencies (`graphql ~> 2.5`, `activesupport`) come with the gem. If y
|
|
|
18
18
|
want federation, add `apollo-federation` to your app — it is intentionally **not**
|
|
19
19
|
a hard dependency of Lighthouse so non-federated apps stay lean.
|
|
20
20
|
|
|
21
|
+
## The install generator
|
|
22
|
+
|
|
23
|
+
```shell
|
|
24
|
+
bin/rails generate lighthouse:install
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
does steps 1–5 below for you:
|
|
28
|
+
|
|
29
|
+
| File | What it is |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| `config/initializers/lighthouse.rb` | Every [configuration](configuration.md) option, commented out at its default |
|
|
32
|
+
| `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 |
|
|
34
|
+
| `app/controllers/graphql_controller.rb` | The endpoint, plus a `post '/graphql'` route |
|
|
35
|
+
| `graphql.config.yml` | IDE configuration — see [IDE support](#ide-support) |
|
|
36
|
+
|
|
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.
|
|
53
|
+
|
|
21
54
|
## 1. Write your schema (SDL)
|
|
22
55
|
|
|
23
56
|
By default Lighthouse loads every `*.graphql` file under `RAILS_ROOT/graphql`,
|
|
@@ -124,6 +157,47 @@ end
|
|
|
124
157
|
|
|
125
158
|
See [configuration.md](configuration.md) for all options.
|
|
126
159
|
|
|
160
|
+
## IDE support
|
|
161
|
+
|
|
162
|
+
Your IDE validates and autocompletes `.graphql` files against the schema it can
|
|
163
|
+
see. Lighthouse's directive definitions and the types its manipulation phase
|
|
164
|
+
generates live in Ruby, so without help the IDE flags `@paginate` as unknown and
|
|
165
|
+
can't resolve `UserPaginator`. PHP Lighthouse solves this with an IDE helper
|
|
166
|
+
command; so does Lighthouse for Rails:
|
|
167
|
+
|
|
168
|
+
```shell
|
|
169
|
+
bin/rails lighthouse:ide_helper # writes to Rails.root
|
|
170
|
+
OUTPUT=tmp/ide bin/rails lighthouse:ide_helper
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
It writes two files:
|
|
174
|
+
|
|
175
|
+
- **`schema-directives.graphql`** — a definition for every registered directive,
|
|
176
|
+
built-in or [your own](custom-directives.md), except those your SDL already
|
|
177
|
+
declares (so the IDE never sees a duplicate).
|
|
178
|
+
- **`programmatic-types.graphql`** — the types Lighthouse adds to the executable
|
|
179
|
+
schema: `<Type>Paginator`, `<Type>SimplePaginator`, `<Type>Connection`/`Edge`,
|
|
180
|
+
`PaginatorInfo`, `SimplePaginatorInfo`, `PageInfo`, the `whereConditions`
|
|
181
|
+
inputs and enums, and the `_` placeholder scalar.
|
|
182
|
+
|
|
183
|
+
Point your IDE's [graphql-config](https://the-guild.dev/graphql/config) at them
|
|
184
|
+
next to your SDL — the install generator writes this `graphql.config.yml`:
|
|
185
|
+
|
|
186
|
+
```yaml
|
|
187
|
+
schema:
|
|
188
|
+
- graphql/**/*.graphql
|
|
189
|
+
- schema-directives.graphql
|
|
190
|
+
- programmatic-types.graphql
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
It works with the GraphQL plugin for JetBrains IDEs and the GraphQL extension for
|
|
194
|
+
VS Code. The files are derived from your schema, so the generator adds them to
|
|
195
|
+
`.gitignore`; rerun the task after changing the schema or registering a directive.
|
|
196
|
+
|
|
197
|
+
> The IDE reads fields as you wrote them: `users: [User!]! @paginate` still looks
|
|
198
|
+
> like a list to it, although clients query `users { data { … } }`. Field
|
|
199
|
+
> rewrites are not something an extra file can express.
|
|
200
|
+
|
|
127
201
|
## Casing: camelCase ↔ snake_case
|
|
128
202
|
|
|
129
203
|
GraphQL conventionally uses `camelCase` field names; Rails uses `snake_case`
|
|
@@ -144,9 +218,19 @@ type User {
|
|
|
144
218
|
}
|
|
145
219
|
```
|
|
146
220
|
|
|
221
|
+
## Custom queries and mutations
|
|
222
|
+
|
|
223
|
+
When a field needs Ruby, write a graphql-ruby resolver or mutation class named
|
|
224
|
+
after it — `Query.latestPosts` resolves to `Queries::LatestPosts`
|
|
225
|
+
(`app/graphql/queries/latest_posts.rb`), `Mutation.publishPost` to
|
|
226
|
+
`Mutations::PublishPost`. `bin/rails generate lighthouse:query LatestPosts`
|
|
227
|
+
scaffolds one. See [resolvers.md](resolvers.md).
|
|
228
|
+
|
|
147
229
|
## Next steps
|
|
148
230
|
|
|
149
231
|
- [Directives reference](directives.md) — the full vocabulary.
|
|
232
|
+
- [Queries, mutations & builders](resolvers.md), [Mutations](mutations.md),
|
|
233
|
+
[Validation](validation.md), [Caching](caching.md).
|
|
150
234
|
- [Relationships](relationships.md) — wiring associations.
|
|
151
235
|
- [Filtering & ordering](filtering-and-ordering.md) — query constraints.
|
|
152
236
|
- [Apollo Federation](federation.md) — become a subgraph.
|