lighthouse-graphql 0.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +139 -1
- data/README.md +8 -5
- data/docs/configuration.md +26 -2
- data/docs/directives.md +287 -4
- data/docs/generators.md +82 -0
- data/docs/getting-started.md +26 -30
- data/docs/relationships.md +42 -0
- data/docs/relay.md +137 -0
- data/docs/resolvers.md +261 -22
- data/docs/security.md +17 -0
- data/docs/types.md +219 -0
- data/docs/uploads.md +80 -0
- data/lib/generators/lighthouse/base_classes.rb +32 -49
- data/lib/generators/lighthouse/install/install_generator.rb +83 -24
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +14 -6
- data/lib/generators/lighthouse/install/templates/schema.rb.tt +10 -3
- data/lib/lighthouse/async_mutation_job.rb +17 -0
- data/lib/lighthouse/complexity.rb +5 -4
- data/lib/lighthouse/config.rb +13 -2
- data/lib/lighthouse/controller.rb +27 -9
- 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/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 +57 -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 +77 -0
- data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +81 -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 +53 -6
- data/lib/lighthouse/generated_resolvers.rb +13 -8
- data/lib/lighthouse/global_id.rb +31 -0
- data/lib/lighthouse/graphql/version.rb +1 -1
- 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 +33 -0
- data/lib/lighthouse/relation_counts.rb +29 -0
- data/lib/lighthouse/resolve_info.rb +80 -0
- data/lib/lighthouse/resolver_bases.rb +9 -2
- data/lib/lighthouse/resolver_dispatch.rb +36 -11
- data/lib/lighthouse/scalar_coercion.rb +72 -0
- data/lib/lighthouse/schema_cache.rb +2 -1
- data/lib/lighthouse/schema_generator.rb +11 -10
- 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 +26 -0
- data/lib/lighthouse/upload_store.rb +32 -0
- data/sig/lighthouse/directives.rbs +3 -2
- data/sig/lighthouse.rbs +18 -0
- metadata +38 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5dd6b5839f6520df6489135c1a37382ce4f7889d136140f652443ad5a3cd1a81
|
|
4
|
+
data.tar.gz: 60fdb10b55abe72a1dc3df89705b904b424344e5bffa611e695a46fa8767ddc7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0f2aff4008f41238a6ebca9c598add10069e7f0c4c8c5578bc1041f3da50f36b859b9ce1c91a29576442562a42afca20845b8fb3704b19d374570381a3588262
|
|
7
|
+
data.tar.gz: b985fc24dfb2d0196c96f84cb0423d0b21e38659a0bc1cd4db13dc3818a8939fd2340006d85605cb5a50fde37f638d177ba90bf5af26e2cc3b9a3a0c2043986a
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,143 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.5.0] - 2026-10-18
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `Lighthouse::GraphQL::Type`, a concern that hands a graphql-ruby type class to Lighthouse, in
|
|
14
|
+
graphql-ruby's `app/graphql/types` structure. A field of an object type with no resolving
|
|
15
|
+
directive resolves with the method of its snake_case name on `Types::<Type>Type`, when that class
|
|
16
|
+
includes the concern and defines it (`Post.readingTime` → `Types::PostType#reading_time`). As in
|
|
17
|
+
graphql-ruby, the method has `object` (the parent), `context`, the `dataload*` helpers, and the
|
|
18
|
+
field's arguments as keywords. The SDL still declares the fields; the ones the class does not
|
|
19
|
+
define are read off the object as before. A type class that does not include the concern is left
|
|
20
|
+
alone. The class is looked up in the new `types` namespace (default `['Types']`), and must inherit
|
|
21
|
+
from `base_types[:object]` when one is given.
|
|
22
|
+
- `@model(class:)`, as PHP Lighthouse's: maps an object type to the ActiveRecord model of another
|
|
23
|
+
name. Every directive taking `model:` (`@all`, `@find`, `@paginate`, `@create`, `@update`, the
|
|
24
|
+
`@can*` family...) on a field returning the type gets it, unless it names its own; unions,
|
|
25
|
+
interfaces and `@node` resolve the model's records to the type; the schema does not build unless
|
|
26
|
+
it is a model; and a type class's methods only run on its records (another parent is reported
|
|
27
|
+
through `on_error`).
|
|
28
|
+
- Unions and interfaces resolve each value to its object type as PHP Lighthouse does: a Hash's
|
|
29
|
+
`__typename`, else the type named after the value's class or a superclass (STI), else the type
|
|
30
|
+
whose `@model` names its model. A union's type class (`Types::SearchResultType` including the
|
|
31
|
+
concern) can take over with graphql-ruby's `self.resolve_type(object, context)`, an interface's
|
|
32
|
+
with `definition_methods { def resolve_type }`, and `@union(resolveType:)` /
|
|
33
|
+
`@interface(resolveType:)` name a class in the SDL. A value nothing resolves, or one resolved
|
|
34
|
+
outside the union, raises `Lighthouse::DefinitionError` naming the ways out.
|
|
35
|
+
- Custom scalars read and write their values through their class: `Types::DateTimeType` (a
|
|
36
|
+
`GraphQL::Schema::Scalar` including the concern), or the class `@scalar(class:)` names, with
|
|
37
|
+
graphql-ruby's `coerce_input` / `coerce_result`. A `GraphQL::CoercionError` makes the argument
|
|
38
|
+
invalid with its message. A scalar with no class passes its values through, as before.
|
|
39
|
+
- `@method` passes the field's arguments to the method: as snake_case keywords when it takes
|
|
40
|
+
keywords, otherwise positionally in SDL order, as PHP Lighthouse does.
|
|
41
|
+
- `base_types[:union]`, `[:interface]` and `[:scalar]`, like `[:object]`, are the bases the type
|
|
42
|
+
classes of their kind must inherit from.
|
|
43
|
+
- `docs/types.md`: unions, interfaces and scalars.
|
|
44
|
+
- `@field(resolver: "Class@method")`, as PHP Lighthouse's: the method replaces `call` on a plain
|
|
45
|
+
class (as a class method, else an instance method), or `resolve` on a resolver class, whose
|
|
46
|
+
`ready?` / `authorized?` still run.
|
|
47
|
+
- `ResolveInfo`, PHP Lighthouse's `$resolveInfo`: `resolve_info` in `Resolvers::` / `Mutations::`
|
|
48
|
+
classes, classes `@field` names and type class methods, and a fourth argument for a plain callable
|
|
49
|
+
that takes one. It has the field, its parent type, object, arguments, context and path, and
|
|
50
|
+
`enhance_builder(relation, scopes: [])`, which applies the field's `@builder` classes and argument
|
|
51
|
+
directives (`@eq`, `@where`, `@orderBy`, `@whereConditions`, ...) to a query the class builds.
|
|
52
|
+
- `@with(relation:, scopes:)` and `@withCount(relation:, scopes:)` eager-load a relation, or its
|
|
53
|
+
count (kept on the parent as `<relation>_count`), before the field resolves; `@lazyLoad(relations:)`
|
|
54
|
+
eager-loads relations of the records a field returns. All three load across every parent in the
|
|
55
|
+
response at once, through the Dataloader, and take nested relations (`"posts.comments"`).
|
|
56
|
+
- Relay global object identification: `@globalId` encodes a field (base64 of `Type:id`) and decodes
|
|
57
|
+
an argument or input field (`decode: ARRAY | TYPE | ID`); `@node` makes a type implement `Node` and
|
|
58
|
+
adds `Query.node(id:)`, which finds the record through the type's model or `@node(resolver:)`. The
|
|
59
|
+
new `global_id` setting takes your own encoder. `docs/relay.md`.
|
|
60
|
+
- A union's or interface's class may answer `[type, value]`, as graphql-ruby allows.
|
|
61
|
+
- File uploads: the `Upload` scalar (Lighthouse's own for `scalar Upload`), the GraphQL multipart
|
|
62
|
+
request specification in `Lighthouse::GraphQL::Controller`, and `@upload(disk:, path:)`, which
|
|
63
|
+
stores the file through the new `upload_store` setting (an ActiveStorage blob by default) and
|
|
64
|
+
passes its signed id. `docs/uploads.md`.
|
|
65
|
+
- Filters: `@whereNull`, `@whereNotNull`, `@whereBetween`, `@whereNotBetween`, `@whereJsonContains`
|
|
66
|
+
(PostgreSQL, MySQL, SQLite), `@scope`, `@nest` (the filters of an input's fields) and `@limit`
|
|
67
|
+
(a LIMIT, or a per-parent cut of a relation's records); `@whereAuth(relation:)` keeps the
|
|
68
|
+
signed-in user's records.
|
|
69
|
+
- Argument directives: `@trim`, `@drop`, `@hash` (BCrypt) and `@bind(class:)`, which replaces an id
|
|
70
|
+
with its record(s) or fails validation. Rewriting ones (`@globalId`, `@trim`, `@drop`) run before
|
|
71
|
+
`@guard`, `@can*` and `@validator`; effectful ones (`@hash`, `@bind`, `@upload`) after them.
|
|
72
|
+
- `@builder` on an argument or input field, as PHP Lighthouse's: the method gets the argument's
|
|
73
|
+
value whenever the client sends it, `null` included. `method: "Class@method"` names the builder
|
|
74
|
+
without `class:`, and `value:` on a field passes a fixed value in place of the arguments.
|
|
75
|
+
- `@limit(builder: true)` puts the LIMIT on the field's query; `@limit` must be on an `Int` argument,
|
|
76
|
+
and is accepted on the field itself, as PHP Lighthouse allows.
|
|
77
|
+
- `@hasManyThrough` and `@hasOneThrough`, PHP Lighthouse's names, as aliases of `@hasMany` and
|
|
78
|
+
`@hasOne`, which resolve `has_many` / `has_one :through`.
|
|
79
|
+
- `ResolveInfo#lookahead`: graphql-ruby's Lookahead for a field resolved by a class (a `Resolvers::`
|
|
80
|
+
class, `@field`, a type class method). It never reaches the arguments, so `@cache` keys are unchanged.
|
|
81
|
+
- `@throttle` logs a warning when the cache store is a `:null_store`, which counts nothing.
|
|
82
|
+
- `@enum(value:)` for an enum value's internal value; `@namespaced` to group fields; `@throttle` to
|
|
83
|
+
rate-limit a field per user or IP (the controller concern now puts `:remote_ip` in the context);
|
|
84
|
+
`@async` to run a mutation in an ActiveJob and answer `true`; `@hide(env:)` / `@show(env:)` to leave
|
|
85
|
+
fields out by environment (the schema cache key now includes it).
|
|
86
|
+
|
|
87
|
+
### Changed
|
|
88
|
+
|
|
89
|
+
- Generators build on graphql-ruby's instead of copying them. `lighthouse:install` runs
|
|
90
|
+
`graphql:install --no-relay --skip-graphiql --skip-query-logs` for the base classes every
|
|
91
|
+
graphql-ruby app has (`Types::Base*`, `Resolvers::BaseResolver`, `Mutations::BaseMutation`), then
|
|
92
|
+
replaces what it wrote that an SDL-first app does differently: the schema class (built from the
|
|
93
|
+
SDL, naming every base in `base_types`), the controller, no `Types::QueryType` /
|
|
94
|
+
`MutationType`, and a `BaseMutation` that is a `GraphQL::Schema::Mutation`. It writes
|
|
95
|
+
`Subscriptions::BaseSubscription` itself, graphql-ruby having none. Files the app had are kept.
|
|
96
|
+
- `docs/generators.md`: every generator, and which of graphql-ruby's fit a Lighthouse app
|
|
97
|
+
(`graphql:object`, `:scalar`, `:union`, `:interface` for type classes; `graphql:mutation` and
|
|
98
|
+
`graphql:detailed_trace` not as they are).
|
|
99
|
+
|
|
100
|
+
- `@field(resolver:)` on a field of a type other than `Query` and `Mutation` looks the class up in
|
|
101
|
+
the resolvers namespace first, nested names included: `@field(resolver: "Posts::TagsCount")`
|
|
102
|
+
resolves to `Resolvers::Posts::TagsCount`. Then it tries `resolver_namespaces`, as before, so
|
|
103
|
+
full names and classes outside `Resolvers::` resolve as they did. A class `Resolvers::X` now
|
|
104
|
+
wins over one that `resolver_namespaces` would find for the same name.
|
|
105
|
+
|
|
106
|
+
### Fixed
|
|
107
|
+
|
|
108
|
+
- A wrapping directive (`@globalId`, `@guard`, `@cache`, ...) on a field of a type whose objects are
|
|
109
|
+
Hashes read nothing: the wrapped default reader only called methods. It now reads the Hash's
|
|
110
|
+
keys, as the default reader does.
|
|
111
|
+
|
|
112
|
+
- A field returning a union or an interface no longer raises `undefined method 'resolve_type' for
|
|
113
|
+
an instance of Proc`. Lighthouse's field dispatcher replaced the schema's default resolve with a
|
|
114
|
+
Proc, which graphql-ruby also asks for `resolve_type` and scalar coercion; it now hands both on.
|
|
115
|
+
|
|
116
|
+
- `@field(resolver:)` on a `Query` or `Mutation` field finds a nested name in the resolvers /
|
|
117
|
+
mutations namespaces, as documented: `"Feeds::Home"` resolves to `Resolvers::Feeds::Home`,
|
|
118
|
+
then to `Feeds::Home`. It used to try only `Feeds::Home`, so the field resolved to `null`.
|
|
119
|
+
The field's `complexity` is looked up the same way.
|
|
120
|
+
|
|
121
|
+
### Documentation
|
|
122
|
+
|
|
123
|
+
- `docs/resolvers.md`: fields of types other than `Query` and `Mutation` resolved with `@field`,
|
|
124
|
+
whose class receives the parent record as `object`. The example app resolves `Post.tagsCount`
|
|
125
|
+
(batched through the dataloader), `Post.excerpt(length:)` and `Query.popularTags` this way.
|
|
126
|
+
It resolves `Post.readingTime` and the `ReadingTime` type, which is not a model, with
|
|
127
|
+
`Types::PostType` and `Types::ReadingTimeType`, `Query.search` (a union of `Post` and `User`)
|
|
128
|
+
and `Query.latestActivity` (an `Authored` interface) by the default, `DateTime` with
|
|
129
|
+
`Types::DateTimeType`, and `Post.summary(words:)` with `@method`. `Tag` is a Relay node with
|
|
130
|
+
global ids; `User.latestPostTitle` uses `@with`, `Tag.usage` `@withCount`, `User.posts`
|
|
131
|
+
`@lazyLoad`, `Query.counts` `@field(resolver: "Statistics@counts")`, and `Query.myPosts`
|
|
132
|
+
`resolve_info.enhance_builder`. `Mutation.updateAvatar` takes a multipart upload with `@upload`
|
|
133
|
+
(the example now loads ActiveStorage). `Mutation.schedulePublish` is `@async` on Solid Queue,
|
|
134
|
+
`Query.searchPosts` is `@throttle`d, and `Query.posts` filters `metadata` with `@whereJsonContains`;
|
|
135
|
+
the example's suite runs on PostgreSQL and MySQL too (`DATABASE_URL`, a new CI job), with Redis.
|
|
136
|
+
|
|
137
|
+
### Removed
|
|
138
|
+
|
|
139
|
+
- `rails g lighthouse:type`: it was graphql-ruby's `graphql:object` (and `:scalar`, `:union`,
|
|
140
|
+
`:interface`) with an `include`. Generate the class with graphql-ruby, then add
|
|
141
|
+
`include Lighthouse::GraphQL::Type`.
|
|
142
|
+
- `lighthouse:query` and `lighthouse:mutation` no longer write `Resolvers::BaseResolver` /
|
|
143
|
+
`Mutations::BaseMutation` when they are missing: graphql-ruby's install writes them, which
|
|
144
|
+
`lighthouse:install` runs. They say so instead.
|
|
145
|
+
|
|
9
146
|
## [0.4.0] - 2026-10-17
|
|
10
147
|
|
|
11
148
|
### Added
|
|
@@ -324,7 +461,8 @@ had been running inside a private monorepo as a path gem before being extracted.
|
|
|
324
461
|
- Config-driven extension seams: `Lighthouse.configure`, `resolver_namespaces`,
|
|
325
462
|
`on_error`, and `DirectiveRegistry.register` for app-defined directives.
|
|
326
463
|
|
|
327
|
-
[Unreleased]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.
|
|
464
|
+
[Unreleased]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.5.0...HEAD
|
|
465
|
+
[0.5.0]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.4.0...v0.5.0
|
|
328
466
|
[0.4.0]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.3.0...v0.4.0
|
|
329
467
|
[0.3.0]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.2.1...v0.3.0
|
|
330
468
|
[0.2.1]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.2.0...v0.2.1
|
data/README.md
CHANGED
|
@@ -78,10 +78,13 @@ Then let the generator set the app up:
|
|
|
78
78
|
bin/rails generate lighthouse:install
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
It
|
|
82
|
-
|
|
83
|
-
`graphql.
|
|
84
|
-
|
|
81
|
+
It runs graphql-ruby's own `graphql:install` for the base classes every graphql-ruby
|
|
82
|
+
app has, then writes Lighthouse's on top: the initializer, a starter schema in
|
|
83
|
+
`graphql/schema.graphql`, the `AppSchema` class built from it, a `GraphqlController`
|
|
84
|
+
with its `POST /graphql` route, and a `graphql.config.yml` for your IDE. Run
|
|
85
|
+
`bin/rails generate lighthouse:install --help` for its options (`--schema`,
|
|
86
|
+
`--sdl-folder`, `--skip-controller`, `--skip-ide`), and see
|
|
87
|
+
[docs/generators.md](docs/generators.md) for every generator, graphql-ruby's included.
|
|
85
88
|
|
|
86
89
|
### IDE support
|
|
87
90
|
|
|
@@ -130,7 +133,7 @@ Prefer to wire it by hand? These are the steps the generator performs.
|
|
|
130
133
|
options: {
|
|
131
134
|
base_types: {
|
|
132
135
|
resolver: Resolvers::BaseResolver, # graphql-ruby's base classes, which
|
|
133
|
-
mutation: Mutations::BaseMutation, # rails g lighthouse:install
|
|
136
|
+
mutation: Mutations::BaseMutation, # rails g lighthouse:install has graphql-ruby write
|
|
134
137
|
subscription: Subscriptions::BaseSubscription
|
|
135
138
|
# object: Types::BaseObject, ...
|
|
136
139
|
}
|
data/docs/configuration.md
CHANGED
|
@@ -38,6 +38,7 @@ different contracts, so they live apart ([resolvers.md](resolvers.md)):
|
|
|
38
38
|
| `events` | `['Events']` | Classes `@event(dispatch:)` names ([events.md](events.md)) |
|
|
39
39
|
| `subscriptions` | `['Subscriptions']` | `Subscriptions::BaseSubscription` classes (a `GraphQL::Schema::Subscription`; the base includes `Lighthouse::GraphQL::Subscription`) for `Subscription` fields, and `@subscription(class:)` ([subscriptions.md](subscriptions.md)) |
|
|
40
40
|
| `analyzers` | `['Analyzers']` | `GraphQL::Analysis::Analyzer` classes (optionally including `Lighthouse::GraphQL::ComplexityAnalyzer`) that `@complexity(resolver:)` names ([security.md](security.md)) |
|
|
41
|
+
| `types` | `['Types']` | `Types::<Name>Type` classes including `Lighthouse::GraphQL::Type`: an object type's, whose methods resolve the fields of the SDL type `<Name>` ([resolvers.md](resolvers.md#type-classes)); a union's or interface's, which picks its values' types; a scalar's, which coerces its values ([types.md](types.md)). Also where `@union`, `@interface` and `@scalar` look classes up |
|
|
41
42
|
|
|
42
43
|
```ruby
|
|
43
44
|
config.namespaces = { queries: ['Api::Resolvers'] } # the other kinds keep their defaults
|
|
@@ -47,8 +48,9 @@ config.namespaces = { queries: ['Api::Resolvers'] } # the other kinds keep the
|
|
|
47
48
|
|
|
48
49
|
An ordered list of namespaces tried for **models** named by a directive
|
|
49
50
|
(`@all(model: "User")`) and for `@field(resolver:)` on types other than `Query`
|
|
50
|
-
and `Mutation
|
|
51
|
-
|
|
51
|
+
and `Mutation`, after the resolvers namespace
|
|
52
|
+
([resolvers.md](resolvers.md#fields-of-other-types)). Fully-qualified names
|
|
53
|
+
(containing `::`) are used as-is. Defaults to `['App::Graphql']`.
|
|
52
54
|
|
|
53
55
|
```ruby
|
|
54
56
|
config.resolver_namespaces = ['App::Graphql', 'Resolvers']
|
|
@@ -115,6 +117,28 @@ initializer.
|
|
|
115
117
|
config.convert_empty_strings_to_null = true
|
|
116
118
|
```
|
|
117
119
|
|
|
120
|
+
### `global_id`
|
|
121
|
+
|
|
122
|
+
Encodes and decodes Relay global ids for [`@globalId` and `@node`](relay.md): any
|
|
123
|
+
object with `encode(type, id)` returning the global id, and `decode(global_id)`
|
|
124
|
+
returning `[type, id]`. Defaults to `Lighthouse::GraphQL::GlobalId`, the base64 of
|
|
125
|
+
`Type:id`, as PHP Lighthouse's.
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
config.global_id = SignedGlobalIds
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### `upload_store`
|
|
132
|
+
|
|
133
|
+
Stores the file an [`@upload`](uploads.md) argument carries: any object with
|
|
134
|
+
`call(file, disk:, path:, public:)`, whose return value the argument then holds.
|
|
135
|
+
Defaults to `Lighthouse::GraphQL::UploadStore`, which makes an ActiveStorage blob and
|
|
136
|
+
returns its signed id.
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
config.upload_store = ->(file, path: '/', **) { S3Uploader.put(file, prefix: path) }
|
|
140
|
+
```
|
|
141
|
+
|
|
118
142
|
### `queue_broadcasts`, `broadcast_queue`
|
|
119
143
|
|
|
120
144
|
Whether [`@broadcast`](directives.md#broadcast) triggers its subscription in an
|
data/docs/directives.md
CHANGED
|
@@ -129,6 +129,7 @@ See [relationships.md](relationships.md) for the full guide.
|
|
|
129
129
|
| `@belongsTo` | inverse of has-one/has-many |
|
|
130
130
|
| `@belongsToMany` | many-to-many: a list, or paginated with `type:` |
|
|
131
131
|
| `@morphTo`, `@morphOne`, `@morphMany`, `@morphToMany` | the polymorphic variants |
|
|
132
|
+
| `@hasManyThrough`, `@hasOneThrough` | PHP Lighthouse's names for `@hasMany` / `@hasOne` on a `has_many` / `has_one :through` |
|
|
132
133
|
|
|
133
134
|
```graphql
|
|
134
135
|
type User {
|
|
@@ -142,6 +143,28 @@ type User {
|
|
|
142
143
|
The relation name defaults to the field name (snake_cased). Override with
|
|
143
144
|
`@hasMany(relation: "authored_posts")`.
|
|
144
145
|
|
|
146
|
+
### `@with`, `@withCount` and `@lazyLoad`
|
|
147
|
+
|
|
148
|
+
Eager loading for fields the relationship directives do not resolve, each across
|
|
149
|
+
every parent in the response at once. See
|
|
150
|
+
[relationships.md](relationships.md#eager-loading-for-other-fields).
|
|
151
|
+
|
|
152
|
+
```graphql
|
|
153
|
+
type User {
|
|
154
|
+
"@with loads the relation before the field resolves, for a method that reads it."
|
|
155
|
+
taskSummary: String! @with(relation: "tasks") @method(name: "task_summary")
|
|
156
|
+
|
|
157
|
+
"@withCount sets posts_count on the user; the field's resolver reads it."
|
|
158
|
+
activity: String! @withCount(relation: "posts", scopes: ["published"]) @method
|
|
159
|
+
|
|
160
|
+
"@lazyLoad loads relations of the records the field returns."
|
|
161
|
+
posts: [Post!]! @hasMany @lazyLoad(relations: ["tags", "comments.author"])
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`relation` / `relations` may be nested with dots. `scopes` narrow the loaded or
|
|
166
|
+
counted records. `@with`'s `relation` defaults to the field's name.
|
|
167
|
+
|
|
145
168
|
---
|
|
146
169
|
|
|
147
170
|
## Field directives
|
|
@@ -162,10 +185,16 @@ type Contact {
|
|
|
162
185
|
```
|
|
163
186
|
|
|
164
187
|
On `Query` and `Mutation` the name is looked up in the queries / mutations
|
|
165
|
-
namespaces,
|
|
188
|
+
namespaces, a nested name too (`Feeds::Home` is `Resolvers::Feeds::Home`), and
|
|
189
|
+
then as written. On any other type it is looked up in the resolvers namespace the
|
|
190
|
+
same way (`Posts::TagsCount` is `Resolvers::Posts::TagsCount`), then in
|
|
191
|
+
[`resolver_namespaces`](configuration.md). There, the class's `object` is the parent: the
|
|
192
|
+
record the field belongs to (see
|
|
193
|
+
[resolvers.md](resolvers.md#fields-of-other-types)). The class may
|
|
166
194
|
be a `GraphQL::Schema::Resolver` (graphql-ruby's contract), or a plain callable —
|
|
167
195
|
a class-level `call`, an instance `#call`, or an instance `#resolve`, each
|
|
168
|
-
receiving `(object, args, context)
|
|
196
|
+
receiving `(object, args, context)`, and the [`ResolveInfo`](resolvers.md#resolveinfo)
|
|
197
|
+
as a fourth argument when it takes one:
|
|
169
198
|
|
|
170
199
|
```ruby
|
|
171
200
|
module Resolvers
|
|
@@ -177,6 +206,17 @@ module Resolvers
|
|
|
177
206
|
end
|
|
178
207
|
```
|
|
179
208
|
|
|
209
|
+
A method after `@` replaces `call`, as PHP Lighthouse's `Class@method`: it is called
|
|
210
|
+
on the class when the class has it, else on an instance. On a
|
|
211
|
+
`GraphQL::Schema::Resolver` it replaces `resolve`, and `ready?` / `authorized?`
|
|
212
|
+
still run.
|
|
213
|
+
|
|
214
|
+
```graphql
|
|
215
|
+
type Query {
|
|
216
|
+
usersTotal: Int @field(resolver: "Statistics@users_total") # Resolvers::Statistics.users_total
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
180
220
|
### `@rename`
|
|
181
221
|
|
|
182
222
|
Read a differently-named attribute off the parent object. This is the explicit
|
|
@@ -191,15 +231,157 @@ type User {
|
|
|
191
231
|
### `@method`
|
|
192
232
|
|
|
193
233
|
Resolve a field by calling a method on the parent object (PHP-parity). Great for
|
|
194
|
-
exposing a computed method or predicate without a custom resolver.
|
|
234
|
+
exposing a computed method or predicate without a custom resolver. `name:`
|
|
235
|
+
defaults to the field's snake_case name.
|
|
195
236
|
|
|
196
237
|
```graphql
|
|
197
238
|
type Conversation {
|
|
198
239
|
labels: [String!]! @method(name: "label_list")
|
|
199
240
|
canReply: Boolean @method(name: "can_reply?")
|
|
241
|
+
summary(words: Int = 20): String! @method
|
|
200
242
|
}
|
|
201
243
|
```
|
|
202
244
|
|
|
245
|
+
The field's arguments reach the method: as snake_case keywords when it takes
|
|
246
|
+
keywords (`def summary(words:)`), otherwise positionally, in the order the SDL
|
|
247
|
+
declares them, as PHP Lighthouse passes them (`def summary(words)`). The SDL's
|
|
248
|
+
defaults apply, and a method taking no parameters is called without them.
|
|
249
|
+
|
|
250
|
+
## Relay
|
|
251
|
+
|
|
252
|
+
### `@globalId` and `@node`
|
|
253
|
+
|
|
254
|
+
`@globalId` encodes a field's value as a Relay global id (base64 of `Type:id`), and
|
|
255
|
+
decodes an argument or input field (`decode: ARRAY | TYPE | ID`). `@node` on a type
|
|
256
|
+
makes it implement `Node` and adds `Query.node(id:)`, which finds the type's model
|
|
257
|
+
record, or calls `@node(resolver:)`. See [relay.md](relay.md).
|
|
258
|
+
|
|
259
|
+
```graphql
|
|
260
|
+
type User @node {
|
|
261
|
+
id: ID! @globalId
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
type Query {
|
|
265
|
+
user(id: ID! @globalId(decode: ID) @eq): User @find
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Execution
|
|
270
|
+
|
|
271
|
+
### `@namespaced`
|
|
272
|
+
|
|
273
|
+
A field that only groups others: it resolves to `true`, and the fields of its type
|
|
274
|
+
resolve with their own directives. On `Query` or `Mutation`.
|
|
275
|
+
|
|
276
|
+
```graphql
|
|
277
|
+
type Query { post: PostQueries! @namespaced }
|
|
278
|
+
type PostQueries {
|
|
279
|
+
find(id: ID! @eq): Post @find
|
|
280
|
+
list: [Post!]! @paginate
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### `@throttle`
|
|
285
|
+
|
|
286
|
+
Rate-limits a field per client: at most `maxAttempts` calls every `decayMinutes`
|
|
287
|
+
(default 60 a minute), counted in [`cache_store`](configuration.md#cache_store). A
|
|
288
|
+
client is the signed-in user, else the context's `:remote_ip`, which the controller
|
|
289
|
+
concern sets. Past the limit the field fails with `Rate limit for Query.x exceeded.
|
|
290
|
+
Try again later.` before anything else runs. `name:` makes fields count together,
|
|
291
|
+
`prefix:` separates groups.
|
|
292
|
+
|
|
293
|
+
```graphql
|
|
294
|
+
type Mutation {
|
|
295
|
+
login(email: String!, password: String!): User @throttle(maxAttempts: 5)
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### `@async`
|
|
300
|
+
|
|
301
|
+
Runs a mutation in a job and answers `true` at once. `@guard`, the `@can*` family,
|
|
302
|
+
`@validator` and the argument directives run in the request, so the client still
|
|
303
|
+
gets their errors; what resolves the field runs in
|
|
304
|
+
`Lighthouse::GraphQL::AsyncMutationJob` (ActiveJob), with the arguments and the
|
|
305
|
+
context values ActiveJob can serialize (records too, through GlobalID). The field
|
|
306
|
+
must be a `Mutation` field returning `Boolean`, and the schema a named constant.
|
|
307
|
+
|
|
308
|
+
```graphql
|
|
309
|
+
type Mutation {
|
|
310
|
+
importFeed(url: String!): Boolean! @guard @async(queue: "imports")
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### `@hide` and `@show`
|
|
315
|
+
|
|
316
|
+
Leave a field out of the schema by environment (`Rails.env`): `@hide(env:)` removes
|
|
317
|
+
it in the environments listed, `@show(env:)` keeps it only in those.
|
|
318
|
+
|
|
319
|
+
```graphql
|
|
320
|
+
type Query {
|
|
321
|
+
debugInfo: String @show(env: ["development", "test"])
|
|
322
|
+
legacyFeed: [Post!]! @hide(env: ["production"]) @all
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Type directives
|
|
327
|
+
|
|
328
|
+
### `@model`
|
|
329
|
+
|
|
330
|
+
Maps an object type to the ActiveRecord model whose records it resolves, when the
|
|
331
|
+
names differ, as PHP Lighthouse's. Every directive that takes `model:` (`@all`,
|
|
332
|
+
`@find`, `@first`, `@paginate`, `@create`, `@update`, `@upsert`, `@delete`, the
|
|
333
|
+
`@can*` family...) on a field returning the type gets it, unless it names its own; a
|
|
334
|
+
union or interface resolves the model's records to the type; `@node` finds them. The
|
|
335
|
+
schema does not build unless the class is an ActiveRecord model.
|
|
336
|
+
|
|
337
|
+
```graphql
|
|
338
|
+
type Article @model(class: "Post") { id: ID! title: String! }
|
|
339
|
+
|
|
340
|
+
type Query {
|
|
341
|
+
articles: [Article!]! @paginate # pages through Post
|
|
342
|
+
article(id: ID! @eq): Article @find
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### `@enum`
|
|
347
|
+
|
|
348
|
+
The internal value of an enum value: what a resolver gets for it in an argument,
|
|
349
|
+
and what a result answers with it for.
|
|
350
|
+
|
|
351
|
+
```graphql
|
|
352
|
+
enum Role {
|
|
353
|
+
ADMIN @enum(value: "admin")
|
|
354
|
+
MEMBER @enum(value: "member")
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
type Query { users(role: Role @eq): [User!]! @all } # filters on "admin"
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
They go on a union, an interface or a scalar, and name the class that resolves or
|
|
361
|
+
coerces its values. Without them, Lighthouse uses the type's class
|
|
362
|
+
(`Types::<Name>Type` including `Lighthouse::GraphQL::Type`), or its default. See
|
|
363
|
+
[types.md](types.md).
|
|
364
|
+
|
|
365
|
+
### `@union` and `@interface`
|
|
366
|
+
|
|
367
|
+
Name the class that picks the object type of each value: `resolve_type(object,
|
|
368
|
+
context)` or `call(object, context)`, returning a type name or a type class.
|
|
369
|
+
Looked up in the types namespace (nested names too), then as written.
|
|
370
|
+
|
|
371
|
+
```graphql
|
|
372
|
+
union SearchResult @union(resolveType: "SearchResultResolver") = Post | User
|
|
373
|
+
interface Authored @interface(resolveType: "AuthoredResolver") { id: ID! }
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### `@scalar`
|
|
377
|
+
|
|
378
|
+
Names the class that reads and writes the scalar's values: `coerce_input(value,
|
|
379
|
+
context)` and `coerce_result(value, context)`, as a `GraphQL::Schema::Scalar`.
|
|
380
|
+
|
|
381
|
+
```graphql
|
|
382
|
+
scalar DateTime @scalar(class: "Types::DateTimeType")
|
|
383
|
+
```
|
|
384
|
+
|
|
203
385
|
---
|
|
204
386
|
|
|
205
387
|
## Authentication & Authorization
|
|
@@ -263,7 +445,40 @@ type Query {
|
|
|
263
445
|
```
|
|
264
446
|
|
|
265
447
|
Builders are looked up in their own namespace, `Builders::` (app/graphql/builders)
|
|
266
|
-
— they are not resolvers. See [resolvers.md](resolvers.md#builders).
|
|
448
|
+
— they are not resolvers. See [resolvers.md](resolvers.md#builders). The class is
|
|
449
|
+
`class:` with `method:` (default `call`), or PHP Lighthouse's `method: "Class@method"`.
|
|
450
|
+
|
|
451
|
+
On a field, the method gets `(relation, args, context)`, or the directive's
|
|
452
|
+
`value:` in place of the arguments. On an argument or an input field, it gets
|
|
453
|
+
`(relation, value, context)` whenever the client sends that argument, an explicit
|
|
454
|
+
`null` included, and not when it is left out:
|
|
455
|
+
|
|
456
|
+
```graphql
|
|
457
|
+
type Query {
|
|
458
|
+
posts(since: DateTime @builder(method: "Recent@since")): [Post!]! @all
|
|
459
|
+
latest: [Post!]! @all @builder(class: "Recent", value: 5)
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
```ruby
|
|
464
|
+
class Builders::Recent
|
|
465
|
+
def self.call(relation, count, _context) = relation.order(created_at: :desc).limit(count)
|
|
466
|
+
def self.since(relation, time, _context) = time ? relation.where(created_at: time..) : relation
|
|
467
|
+
end
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### `@whereAuth`
|
|
471
|
+
|
|
472
|
+
Keep only the records the signed-in user owns, through a relation of the model: a
|
|
473
|
+
`belongs_to`, a `has_many`, a `:through`. A guest gets none. It applies where
|
|
474
|
+
`@builder` does (`@all`, `@paginate`, `@first`, `@count`, `enhance_builder`).
|
|
475
|
+
|
|
476
|
+
```graphql
|
|
477
|
+
type Query {
|
|
478
|
+
myPosts: [Post!]! @paginate @whereAuth(relation: "author")
|
|
479
|
+
myTeams: [Team!]! @all @whereAuth(relation: "members")
|
|
480
|
+
}
|
|
481
|
+
```
|
|
267
482
|
|
|
268
483
|
```ruby
|
|
269
484
|
module Builders
|
|
@@ -292,6 +507,10 @@ Full guide: [filtering-and-ordering.md](filtering-and-ordering.md).
|
|
|
292
507
|
| `@like` | `column LIKE %value%` |
|
|
293
508
|
| `@whereKey` | `primary key = value` (`IN` for a list) |
|
|
294
509
|
| `@where(operator:)` | generic operator (`GT`, `GTE`, `IN`, `IS_NULL`, `CONTAINS`, …) |
|
|
510
|
+
| `@whereNull` / `@whereNotNull` | `true`: `column IS NULL` / `IS NOT NULL`; `false`: the opposite |
|
|
511
|
+
| `@whereBetween` / `@whereNotBetween` | `column BETWEEN` / `NOT BETWEEN` the input's two fields, in order |
|
|
512
|
+
| `@whereJsonContains` | a JSON column contains the value (every value of a list); `key: "meta->tags"` points into it |
|
|
513
|
+
| `@scope(name:)` | calls the model scope with the value (`name:` defaults to the argument's) |
|
|
295
514
|
| `@orderBy` | sort by one or more columns |
|
|
296
515
|
|
|
297
516
|
```graphql
|
|
@@ -307,6 +526,41 @@ type Query {
|
|
|
307
526
|
|
|
308
527
|
The column defaults to the snake_cased argument name; override with `key:`.
|
|
309
528
|
|
|
529
|
+
```graphql
|
|
530
|
+
type Query {
|
|
531
|
+
posts(
|
|
532
|
+
unpublished: Boolean @whereNull(key: "published_at")
|
|
533
|
+
createdAt: DateRange @whereBetween(key: "created_at")
|
|
534
|
+
tagged: [String!] @whereJsonContains(key: "tags")
|
|
535
|
+
trending: Boolean @scope
|
|
536
|
+
filter: PostFilter @nest
|
|
537
|
+
limit: Int @limit
|
|
538
|
+
): [Post!]! @all
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
input DateRange { from: DateTime! to: DateTime! }
|
|
542
|
+
input PostFilter { title: String @like, authorId: ID @eq(key: "author_id") }
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
### `@nest`
|
|
546
|
+
|
|
547
|
+
On an argument whose type is an input object, applies the filters of the input's
|
|
548
|
+
fields (at any depth, for those that carry `@nest` too), as if they were the
|
|
549
|
+
field's own arguments.
|
|
550
|
+
|
|
551
|
+
### `@limit`
|
|
552
|
+
|
|
553
|
+
On an `Int` argument, caps how many results the field returns at the client's
|
|
554
|
+
value; `null` leaves them alone. A query not run yet gets a `LIMIT`; the records of
|
|
555
|
+
a relation, loaded for every parent at once, are cut per parent. A negative value is
|
|
556
|
+
an error.
|
|
557
|
+
|
|
558
|
+
`@limit(builder: true)` puts the `LIMIT` on the field's query instead, with the
|
|
559
|
+
other argument directives. Use it on root fields: on a relation batched for many
|
|
560
|
+
parents it would cut them all together, as PHP Lighthouse warns. PHP Lighthouse
|
|
561
|
+
also allows `@limit` on the field itself, where it only marks the field the argument
|
|
562
|
+
limits; it is accepted there and changes nothing.
|
|
563
|
+
|
|
310
564
|
### `@whereConditions` / `@whereHasConditions`
|
|
311
565
|
|
|
312
566
|
Give clients a structured, dynamic `WHERE` builder (AND/OR/operators, and HAS for
|
|
@@ -319,6 +573,35 @@ type Query {
|
|
|
319
573
|
}
|
|
320
574
|
```
|
|
321
575
|
|
|
576
|
+
### `@trim`, `@hash`, `@drop`, `@bind` and `@upload`
|
|
577
|
+
|
|
578
|
+
Rewrite an argument's or input field's value before the field resolves, in the
|
|
579
|
+
order the SDL lists them:
|
|
580
|
+
|
|
581
|
+
| Directive | The resolver gets |
|
|
582
|
+
| --- | --- |
|
|
583
|
+
| `@trim` | the string without leading and trailing whitespace. On a field: every string it takes, nested inputs included |
|
|
584
|
+
| `@drop` | nothing: the argument is removed |
|
|
585
|
+
| `@hash` | the value hashed with BCrypt (the `bcrypt` gem, as `has_secure_password`) |
|
|
586
|
+
| `@bind(class:, column: "id", with: [], optional: false)` | the model record(s) the value identifies, eager-loading `with`; or what a class's `call(value, context)` returns |
|
|
587
|
+
| `@upload(disk:, path: "/")` | what [`upload_store`](configuration.md#upload_store) answers for the uploaded file: the signed id of an ActiveStorage blob, by default ([uploads.md](uploads.md)) |
|
|
588
|
+
|
|
589
|
+
```graphql
|
|
590
|
+
type Mutation {
|
|
591
|
+
signUp(email: String! @trim, password: String! @hash, captcha: String @drop): User! @create
|
|
592
|
+
assign(task: ID! @bind(class: "Task"), user: ID! @bind(class: "User", with: ["roles"])): Task!
|
|
593
|
+
updateAvatar(avatar: Upload! @upload(path: "avatars")): User! @update @inject(context: "current_user.id", name: "id")
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
`@bind` fails the field with a validation error on the argument (`The selected task
|
|
598
|
+
is invalid.`) when no record matches, unless `optional: true`, which passes `null`.
|
|
599
|
+
|
|
600
|
+
`@globalId`, `@trim` and `@drop` only rewrite values: they run first, so `@guard`,
|
|
601
|
+
the `@can*` family and `@validator` see the result. `@hash`, `@bind` and `@upload`
|
|
602
|
+
run once those have let the call through: a validator sees the password before it
|
|
603
|
+
is hashed, and nothing is stored or looked up for a refused call.
|
|
604
|
+
|
|
322
605
|
### `@convertEmptyStringsToNull`
|
|
323
606
|
|
|
324
607
|
Replaces incoming empty strings `""` with `null`, before any resolver sees them —
|