lighthouse-graphql 0.1.0 → 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 +168 -1
- data/README.md +46 -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 +52 -64
- 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/resolvers.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Queries, Mutations and Builders
|
|
2
|
+
|
|
3
|
+
Most fields need no Ruby: a directive (`@all`, `@find`, `@paginate`, `@create`,
|
|
4
|
+
…) or the default attribute reader resolves them. For the rest, Lighthouse looks
|
|
5
|
+
up a class by name. There are three kinds of class, and they are not
|
|
6
|
+
interchangeable. Each kind has its own contract and its own folder:
|
|
7
|
+
|
|
8
|
+
| Kind | Folder / namespace | Contract | Used by |
|
|
9
|
+
| --- | --- | --- | --- |
|
|
10
|
+
| Query | `app/graphql/queries` · `Queries::` | `GraphQL::Schema::Resolver` | `Query` fields |
|
|
11
|
+
| Mutation | `app/graphql/mutations` · `Mutations::` | `GraphQL::Schema::Mutation` | `Mutation` fields |
|
|
12
|
+
| Builder | `app/graphql/builders` · `Builders::` | `call(relation, args, context)` → relation | `@builder` |
|
|
13
|
+
|
|
14
|
+
Validators (`app/graphql/validators`) are the fourth kind. They are covered in
|
|
15
|
+
[validation.md](validation.md).
|
|
16
|
+
|
|
17
|
+
## Queries and mutations: graphql-ruby's contract
|
|
18
|
+
|
|
19
|
+
A query or mutation class is a plain graphql-ruby
|
|
20
|
+
[resolver](https://graphql-ruby.org/fields/resolvers.html) or
|
|
21
|
+
[mutation](https://graphql-ruby.org/mutations/mutation_classes.html), so everything
|
|
22
|
+
you know from graphql-ruby applies:
|
|
23
|
+
|
|
24
|
+
- the field's arguments arrive as `snake_case` keyword arguments to `resolve`;
|
|
25
|
+
- `object` and `context` are available;
|
|
26
|
+
- `ready?` and `authorized?` run before `resolve`.
|
|
27
|
+
|
|
28
|
+
The SDL already declares the field's arguments and return type, so the class
|
|
29
|
+
declares neither.
|
|
30
|
+
|
|
31
|
+
```graphql
|
|
32
|
+
type Query {
|
|
33
|
+
latestPosts(limit: Int): [Post!]!
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type Mutation {
|
|
37
|
+
publishPost(id: ID!): Post
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
# app/graphql/queries/latest_posts.rb
|
|
43
|
+
module Queries
|
|
44
|
+
class LatestPosts < GraphQL::Schema::Resolver
|
|
45
|
+
def resolve(limit: 10)
|
|
46
|
+
Post.order(created_at: :desc).limit(limit)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# app/graphql/mutations/publish_post.rb
|
|
52
|
+
module Mutations
|
|
53
|
+
class PublishPost < GraphQL::Schema::Mutation
|
|
54
|
+
def authorized?(id:)
|
|
55
|
+
context[:current_user]&.admin? || raise(GraphQL::ExecutionError, 'Admins only.')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def resolve(id:)
|
|
59
|
+
Post.find(id).tap { |post| post.update!(published: true) }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Resolution by convention
|
|
66
|
+
|
|
67
|
+
A field on `Query` or `Mutation` with **no resolving directive** resolves to the
|
|
68
|
+
class named after it, as in PHP Lighthouse:
|
|
69
|
+
|
|
70
|
+
- `Query.latestPosts` resolves to `Queries::LatestPosts`;
|
|
71
|
+
- `Mutation.publishPost` resolves to `Mutations::PublishPost`.
|
|
72
|
+
|
|
73
|
+
The class is looked up on each call, so code reloading works. Classes found by
|
|
74
|
+
convention must inherit from `GraphQL::Schema::Resolver` (or `Mutation`, which
|
|
75
|
+
is one). Otherwise Lighthouse raises a `Lighthouse::DefinitionError`. A root
|
|
76
|
+
field with no class is reported through [`on_error`](configuration.md#on_error):
|
|
77
|
+
it raises in test and logs elsewhere. Lighthouse never falls back to a top-level
|
|
78
|
+
constant, so a `post` field is never mistaken for the `Post` model.
|
|
79
|
+
|
|
80
|
+
graphql-ruby's `RelayClassicMutation` (what `rails g graphql:install` generates
|
|
81
|
+
`Mutations::BaseMutation` from) works too, as long as the SDL field takes the
|
|
82
|
+
`input:` argument it expects.
|
|
83
|
+
|
|
84
|
+
Wrapping directives (`@guard`, the `@can*` family, `@validator`, `@cache`,
|
|
85
|
+
`@clearCache`) are not resolvers, so a field carrying only those still resolves by
|
|
86
|
+
convention:
|
|
87
|
+
|
|
88
|
+
```graphql
|
|
89
|
+
type Mutation {
|
|
90
|
+
publishPost(id: ID!): Post @guard @canFind(ability: "publish", find: "id")
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Naming a class explicitly
|
|
95
|
+
|
|
96
|
+
`@field(resolver:)` points a field at a class by name. On `Query` and `Mutation`
|
|
97
|
+
the name is looked up in the queries and mutations namespaces; on any other type,
|
|
98
|
+
in [`resolver_namespaces`](configuration.md#resolver_namespaces).
|
|
99
|
+
|
|
100
|
+
```graphql
|
|
101
|
+
type Query {
|
|
102
|
+
feed: [Post!]! @field(resolver: "Feeds::Home") # Queries::Feeds::Home
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Besides graphql-ruby resolver classes, `@field` also accepts a plain callable:
|
|
107
|
+
anything with `call(object, args, context)`.
|
|
108
|
+
|
|
109
|
+
### `@spread` and `@inject`
|
|
110
|
+
|
|
111
|
+
Both shape the keyword arguments a class receives. They do the same for
|
|
112
|
+
[`@create` / `@update`](mutations.md).
|
|
113
|
+
|
|
114
|
+
```graphql
|
|
115
|
+
type Mutation {
|
|
116
|
+
createDraft(input: CreateDraftInput! @spread): Post
|
|
117
|
+
@inject(context: "current_user.id", name: "author_id")
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`resolve(title:, body:, author_id:)` receives the input's fields at the top level.
|
|
122
|
+
`author_id` comes from `context[:current_user].id`, so clients cannot set it.
|
|
123
|
+
`@inject` is repeatable, and `name:` may be a dot path into an input
|
|
124
|
+
(`"input.author_id"`).
|
|
125
|
+
|
|
126
|
+
## Builders
|
|
127
|
+
|
|
128
|
+
A builder is not a resolver: it does not produce the field's value. It refines the
|
|
129
|
+
query of a field that a model directive resolves. It receives the relation, the
|
|
130
|
+
field's arguments and the context, and returns a relation:
|
|
131
|
+
|
|
132
|
+
```graphql
|
|
133
|
+
type Query {
|
|
134
|
+
posts: [Post!]! @paginate @builder(class: "Published")
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
# app/graphql/builders/published.rb
|
|
140
|
+
module Builders
|
|
141
|
+
class Published
|
|
142
|
+
def self.call(relation, args, context)
|
|
143
|
+
relation.where(published: true)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`@builder` works with `@all`, `@paginate`, `@first`, `@count` and `@aggregate`.
|
|
150
|
+
It is repeatable, and `method:` names a method other than `call`. Builder classes
|
|
151
|
+
are looked up in the builders namespace only. Raising a `GraphQL::ExecutionError`
|
|
152
|
+
from a builder (e.g. a tenancy check) always reaches the client; it never falls
|
|
153
|
+
back to the unscoped relation.
|
|
154
|
+
|
|
155
|
+
## Generators
|
|
156
|
+
|
|
157
|
+
```shell
|
|
158
|
+
bin/rails generate lighthouse:query LatestPosts # app/graphql/queries/latest_posts.rb
|
|
159
|
+
bin/rails generate lighthouse:mutation PublishPost # app/graphql/mutations/publish_post.rb
|
|
160
|
+
bin/rails generate lighthouse:builder Published # app/graphql/builders/published.rb
|
|
161
|
+
bin/rails generate lighthouse:validator CreatePostInput # app/graphql/validators/create_post_input_validator.rb
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Each one writes the class with its contract, into the first namespace configured
|
|
165
|
+
for its kind (or `--namespace=…`). Namespaced names nest modules:
|
|
166
|
+
`lighthouse:mutation Blog::Publish` writes `Mutations::Blog::Publish`. These
|
|
167
|
+
generators do not collide with graphql-ruby's `graphql:*` generators. A
|
|
168
|
+
`lighthouse:mutation` and a `graphql:mutation` of the same name target the same
|
|
169
|
+
file, and Rails asks before overwriting it.
|
|
170
|
+
|
|
171
|
+
## Changing the namespaces
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
Lighthouse.configure do |config|
|
|
175
|
+
config.namespaces = { queries: ['Api::Queries'], mutations: ['Api::Mutations'] }
|
|
176
|
+
end
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Kinds you leave out keep their defaults. Each kind takes a list, tried in order.
|
data/docs/validation.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Validation
|
|
2
|
+
|
|
3
|
+
Rails puts validation in the model, and so does Lighthouse: the
|
|
4
|
+
[mutation directives](mutations.md) save through ActiveRecord, so `validates`
|
|
5
|
+
rules apply with no configuration. Not every rule belongs to a model, though:
|
|
6
|
+
a password confirmation, a rule across two arguments, the arguments of a query, or
|
|
7
|
+
input that never reaches a model at all. For those, `@validator` runs a contract
|
|
8
|
+
class before the field resolves.
|
|
9
|
+
|
|
10
|
+
Both report errors the same way, in PHP Lighthouse's format: messages keyed by
|
|
11
|
+
argument path under `extensions.validation`.
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"data": { "signUp": null },
|
|
16
|
+
"errors": [{
|
|
17
|
+
"message": "Validation failed for the field [signUp].",
|
|
18
|
+
"extensions": { "validation": { "passwordConfirmation": ["must match password"] } }
|
|
19
|
+
}]
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Model validations
|
|
24
|
+
|
|
25
|
+
Nothing to do. A failed `save` in `@create`, `@update`, `@createMany` or
|
|
26
|
+
`@updateMany` reports the model's `full_message`s, keyed by the argument each
|
|
27
|
+
attribute came from (see [mutations.md](mutations.md#validation-errors)).
|
|
28
|
+
|
|
29
|
+
## `@validator`
|
|
30
|
+
|
|
31
|
+
```graphql
|
|
32
|
+
directive @validator(class: String) on FIELD_DEFINITION | INPUT_OBJECT
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### On a field
|
|
36
|
+
|
|
37
|
+
The contract receives all of the field's arguments:
|
|
38
|
+
|
|
39
|
+
```graphql
|
|
40
|
+
type Mutation {
|
|
41
|
+
signUp(email: String!, password: String!, passwordConfirmation: String): User @validator
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
# app/graphql/validators/mutation/sign_up_validator.rb
|
|
47
|
+
module Validators
|
|
48
|
+
module Mutation
|
|
49
|
+
class SignUpValidator < Dry::Validation::Contract
|
|
50
|
+
params do
|
|
51
|
+
required(:email).filled(:string)
|
|
52
|
+
required(:password).filled(:string, min_size?: 12)
|
|
53
|
+
optional(:password_confirmation).maybe(:string)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
rule(:password_confirmation) do
|
|
57
|
+
key.failure('must match password') if value && value != values[:password]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The class defaults to `Validators::<ParentType>::<FieldName>Validator`. Name
|
|
65
|
+
another with `@validator(class: "SignUpRules")`; it is looked up in the validators
|
|
66
|
+
namespace.
|
|
67
|
+
|
|
68
|
+
### On an input type
|
|
69
|
+
|
|
70
|
+
The contract receives that input's values. It runs **wherever the type is used**:
|
|
71
|
+
as an argument, nested inside another input, or as each item of a list. The
|
|
72
|
+
fields that take the input need no directive.
|
|
73
|
+
|
|
74
|
+
```graphql
|
|
75
|
+
input AddressInput @validator {
|
|
76
|
+
street: String
|
|
77
|
+
zipCode: String
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type Mutation {
|
|
81
|
+
ship(to: AddressInput!, billing: BillingInput): Shipment
|
|
82
|
+
shipMany(to: [AddressInput!]!): [Shipment!]!
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The class defaults to `Validators::<InputType>Validator`, here
|
|
87
|
+
`Validators::AddressInputValidator`. Errors are prefixed with the path to the
|
|
88
|
+
input: `to.zipCode`, `billing.address.street`, `to.1.street`.
|
|
89
|
+
|
|
90
|
+
### Contracts
|
|
91
|
+
|
|
92
|
+
A contract is a [Dry::Validation](https://dry-rb.org/gems/dry-validation/)
|
|
93
|
+
contract:
|
|
94
|
+
|
|
95
|
+
- Values arrive with `snake_case` symbol keys, and the error keys are mapped back
|
|
96
|
+
to the GraphQL argument names.
|
|
97
|
+
- Declare `option :graphql_context, optional: true` to read the GraphQL context in
|
|
98
|
+
rules (e.g. `graphql_context[:current_user]`).
|
|
99
|
+
- `dry-validation` is an optional dependency. Add it to your Gemfile to use these
|
|
100
|
+
contracts.
|
|
101
|
+
|
|
102
|
+
Any other object works if its `call(values)` returns a Dry-style result, or a
|
|
103
|
+
`{ key => [messages] }` Hash (empty when valid):
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
module Validators
|
|
107
|
+
class NoSpam
|
|
108
|
+
def call(values)
|
|
109
|
+
values[:title].to_s.include?('spam') ? { title: ['looks like spam'] } : {}
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
A missing class raises `Lighthouse::DefinitionError`. Lighthouse never silently
|
|
116
|
+
skips validation.
|
|
117
|
+
|
|
118
|
+
### Order
|
|
119
|
+
|
|
120
|
+
Validation runs after authentication and authorization (`@guard`, `@can*`), so
|
|
121
|
+
anonymous clients cannot probe your rules, and before `@cache` and the resolver.
|
|
122
|
+
All contracts on a field run, and their errors are reported together.
|
|
123
|
+
|
|
124
|
+
Generate a contract with `bin/rails generate lighthouse:validator AddressInput`.
|
|
125
|
+
|
|
126
|
+
## Why not `@rules`?
|
|
127
|
+
|
|
128
|
+
PHP Lighthouse's `@rules(apply: ["email", "max:280"])` embeds Laravel's
|
|
129
|
+
string-based rule language in the schema. Rails has no equivalent to port: its
|
|
130
|
+
validations live in models, and Dry contracts express the rest as Ruby with
|
|
131
|
+
proper types. So Lighthouse for Rails supports those two, and does not invent a
|
|
132
|
+
third rule syntax.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'class_generator'
|
|
4
|
+
|
|
5
|
+
# `rails generate lighthouse:builder PublishedPosts` — a query builder for
|
|
6
|
+
# `@builder(class: "PublishedPosts")`.
|
|
7
|
+
class Lighthouse::Generators::BuilderGenerator < Lighthouse::Generators::ClassGenerator
|
|
8
|
+
desc 'Generate a query builder for @builder in app/graphql/builders.'
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def kind = :builders
|
|
13
|
+
|
|
14
|
+
def body
|
|
15
|
+
<<~RUBY
|
|
16
|
+
# Applied by @builder(class: "#{class_name}") before the field resolves:
|
|
17
|
+
# receives the relation, the field's arguments and the GraphQL context, and
|
|
18
|
+
# returns the relation to use.
|
|
19
|
+
class #{class_basename}
|
|
20
|
+
def self.call(relation, args, context)
|
|
21
|
+
relation
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
RUBY
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def usage
|
|
28
|
+
"Use it with @builder(class: \"#{class_name}\") on a field resolved by @all, @paginate, @first, @count or @aggregate."
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
require 'lighthouse-graphql'
|
|
5
|
+
|
|
6
|
+
# Base for the generators that scaffold the classes a schema refers to by
|
|
7
|
+
# name — queries, mutations, builders and validators — each in its own
|
|
8
|
+
# namespace (Config.namespaces) and with its own contract.
|
|
9
|
+
class Lighthouse::Generators::ClassGenerator < Rails::Generators::NamedBase
|
|
10
|
+
hide!
|
|
11
|
+
|
|
12
|
+
class_option :namespace, type: :string,
|
|
13
|
+
desc: 'Namespace to generate into (default: the first of Config.namespaces for this kind)'
|
|
14
|
+
|
|
15
|
+
def create_class
|
|
16
|
+
create_file File.join('app/graphql', *modules.map(&:underscore), "#{class_basename.underscore}.rb"), source
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def show_usage
|
|
20
|
+
say_status :usage, usage, :blue if usage
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# :queries, :mutations, :builders or :validators.
|
|
26
|
+
def kind
|
|
27
|
+
raise NotImplementedError
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def body
|
|
31
|
+
raise NotImplementedError
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def usage
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def class_basename
|
|
39
|
+
class_name.demodulize
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def namespace_name
|
|
43
|
+
options[:namespace] || Lighthouse::Config.namespaces[kind].first
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def modules
|
|
47
|
+
namespace_name.split('::') + class_name.split('::')[0...-1]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def full_class_name
|
|
51
|
+
(modules + [class_basename]).join('::')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def source
|
|
55
|
+
lines = ['# frozen_string_literal: true', '']
|
|
56
|
+
modules.each_with_index { |mod, depth| lines << "#{' ' * depth}module #{mod}" }
|
|
57
|
+
body.each_line { |line| lines << (line.strip.empty? ? '' : "#{' ' * modules.size}#{line.chomp}") }
|
|
58
|
+
modules.size.times.reverse_each { |depth| lines << "#{' ' * depth}end" }
|
|
59
|
+
"#{lines.join("\n")}\n"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
require 'lighthouse-graphql'
|
|
5
|
+
|
|
6
|
+
# `rails generate lighthouse:install` — sets a Rails app up for Lighthouse:
|
|
7
|
+
# the initializer, a starter SDL file, the schema class, a controller and
|
|
8
|
+
# its route, plus the graphql-config file IDEs read. Rerunning it skips the
|
|
9
|
+
# route, SDL and .gitignore entries that are already in place.
|
|
10
|
+
#
|
|
11
|
+
# It coexists with graphql-ruby's `graphql:install`, which writes the same
|
|
12
|
+
# controller path and route: an existing schema class or controller is kept
|
|
13
|
+
# (pass --force to replace it) instead of overwritten, so an app already
|
|
14
|
+
# serving a graphql-ruby schema keeps doing so until it is pointed at ours.
|
|
15
|
+
class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
|
|
16
|
+
CONTROLLER = 'app/controllers/graphql_controller.rb'
|
|
17
|
+
|
|
18
|
+
source_root File.expand_path('templates', __dir__)
|
|
19
|
+
|
|
20
|
+
desc 'Install Lighthouse: initializer, starter schema, schema class, controller, route and IDE config.'
|
|
21
|
+
|
|
22
|
+
class_option :schema, type: :string, default: 'AppSchema',
|
|
23
|
+
desc: 'Name of the schema constant to generate'
|
|
24
|
+
class_option :sdl_folder, type: :string, default: 'graphql',
|
|
25
|
+
desc: 'Folder, relative to the app root, holding the *.graphql files'
|
|
26
|
+
class_option :skip_controller, type: :boolean, default: false,
|
|
27
|
+
desc: 'Skip the GraphqlController and its route'
|
|
28
|
+
class_option :skip_ide, type: :boolean, default: false,
|
|
29
|
+
desc: 'Skip graphql.config.yml and the .gitignore entries for the IDE helper files'
|
|
30
|
+
|
|
31
|
+
def create_initializer
|
|
32
|
+
template 'initializer.rb.tt', 'config/initializers/lighthouse.rb'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def create_starter_schema
|
|
36
|
+
if Dir[File.join(destination_root, sdl_folder, '**', '*.graphql')].any?
|
|
37
|
+
say_status :skip, "#{sdl_folder}/ already has .graphql files", :yellow
|
|
38
|
+
return
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
template 'schema.graphql.tt', File.join(sdl_folder, 'schema.graphql')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def create_schema_class
|
|
45
|
+
path = File.join('app/graphql', "#{schema_name.underscore}.rb")
|
|
46
|
+
return if keep_existing?(path)
|
|
47
|
+
|
|
48
|
+
template 'schema.rb.tt', path
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def create_controller
|
|
52
|
+
return if options[:skip_controller]
|
|
53
|
+
return if (@kept_controller = keep_existing?(CONTROLLER))
|
|
54
|
+
|
|
55
|
+
template 'graphql_controller.rb.tt', CONTROLLER
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def add_route
|
|
59
|
+
return if options[:skip_controller]
|
|
60
|
+
|
|
61
|
+
routes = File.join(destination_root, 'config/routes.rb')
|
|
62
|
+
return unless File.exist?(routes)
|
|
63
|
+
return if File.read(routes).include?('graphql#execute')
|
|
64
|
+
|
|
65
|
+
# Byte-identical to graphql-ruby's install route, so whichever generator
|
|
66
|
+
# runs second finds it in place and does not add it again.
|
|
67
|
+
route 'post "/graphql", to: "graphql#execute"'
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def create_ide_config
|
|
71
|
+
return if options[:skip_ide]
|
|
72
|
+
|
|
73
|
+
template 'graphql.config.yml.tt', 'graphql.config.yml'
|
|
74
|
+
|
|
75
|
+
gitignore = File.join(destination_root, '.gitignore')
|
|
76
|
+
return unless File.exist?(gitignore)
|
|
77
|
+
|
|
78
|
+
entries = ide_helper_files.map { |file| "/#{file}" } - File.read(gitignore).lines.map(&:strip)
|
|
79
|
+
return if entries.empty?
|
|
80
|
+
|
|
81
|
+
append_to_file '.gitignore', "\n# Lighthouse IDE helper output (bin/rails lighthouse:ide_helper)\n#{entries.join("\n")}\n"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def show_next_steps
|
|
85
|
+
serve =
|
|
86
|
+
if @kept_controller
|
|
87
|
+
"Serve it: #{CONTROLLER} was kept — have it call #{schema_name}.execute(...) to serve Lighthouse's schema."
|
|
88
|
+
else
|
|
89
|
+
'Query it: POST /graphql {"query": "{ users { data { id name } paginatorInfo { total } } }"}'
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
say <<~MSG
|
|
93
|
+
|
|
94
|
+
Lighthouse is installed. Next steps:
|
|
95
|
+
1. Describe your API in #{sdl_folder}/*.graphql (the starter schema assumes a User model).
|
|
96
|
+
2. Generate the IDE helper files: bin/rails lighthouse:ide_helper
|
|
97
|
+
3. #{serve}
|
|
98
|
+
MSG
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
# Files another generator may own (graphql-ruby's install writes the same
|
|
104
|
+
# controller) are left alone unless --force is given.
|
|
105
|
+
def keep_existing?(path)
|
|
106
|
+
return false if options[:force] || !File.exist?(File.join(destination_root, path))
|
|
107
|
+
|
|
108
|
+
say_status :skip, "#{path} already exists (pass --force to replace it)", :yellow
|
|
109
|
+
true
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def schema_name
|
|
113
|
+
options[:schema].camelize
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def sdl_folder
|
|
117
|
+
options[:sdl_folder].delete_suffix('/')
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def default_sdl_folder?
|
|
121
|
+
sdl_folder == 'graphql'
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def ide_helper_files
|
|
125
|
+
[Lighthouse::IdeHelper::DIRECTIVES_FILE, Lighthouse::IdeHelper::PROGRAMMATIC_TYPES_FILE]
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# graphql-config, read by the GraphQL plugins for JetBrains IDEs and VS Code.
|
|
2
|
+
# The two generated files describe Lighthouse's directives and the types it
|
|
3
|
+
# generates; refresh them after changing the schema:
|
|
4
|
+
# bin/rails lighthouse:ide_helper
|
|
5
|
+
schema:
|
|
6
|
+
- <%= sdl_folder %>/**/*.graphql
|
|
7
|
+
- <%= Lighthouse::IdeHelper::DIRECTIVES_FILE %>
|
|
8
|
+
- <%= Lighthouse::IdeHelper::PROGRAMMATIC_TYPES_FILE %>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class GraphqlController < ApplicationController
|
|
4
|
+
# If this endpoint is called from outside this domain, null the session
|
|
5
|
+
# instead of failing CSRF verification:
|
|
6
|
+
# protect_from_forgery with: :null_session
|
|
7
|
+
|
|
8
|
+
def execute
|
|
9
|
+
# PersistedQueries.execute resolves Apollo-style persisted queries when
|
|
10
|
+
# config.persisted_queries is on, and otherwise runs params[:query] as is.
|
|
11
|
+
result = Lighthouse::GraphQL::PersistedQueries.execute(
|
|
12
|
+
<%= schema_name %>,
|
|
13
|
+
query: params[:query],
|
|
14
|
+
extensions: params[:extensions],
|
|
15
|
+
variables: prepare_variables(params[:variables]),
|
|
16
|
+
operation_name: params[:operationName],
|
|
17
|
+
context: {
|
|
18
|
+
# Read by @guard and the @can* directives — see config/initializers/lighthouse.rb.
|
|
19
|
+
# current_user: current_user
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
# The Cache-Control header the selected fields' @cacheControl hints allow
|
|
23
|
+
# (a no-op when the schema has none).
|
|
24
|
+
Lighthouse::GraphQL::CacheControl.apply(response, result)
|
|
25
|
+
render json: result
|
|
26
|
+
rescue StandardError => e
|
|
27
|
+
raise e unless Rails.env.development?
|
|
28
|
+
|
|
29
|
+
handle_error_in_development(e)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# Variables arrive as a JSON string (GET or form posts) or as parameters.
|
|
35
|
+
def prepare_variables(variables_param)
|
|
36
|
+
case variables_param
|
|
37
|
+
when String
|
|
38
|
+
variables_param.present? ? JSON.parse(variables_param) || {} : {}
|
|
39
|
+
when Hash
|
|
40
|
+
variables_param
|
|
41
|
+
when ActionController::Parameters
|
|
42
|
+
variables_param.to_unsafe_hash
|
|
43
|
+
when nil
|
|
44
|
+
{}
|
|
45
|
+
else
|
|
46
|
+
raise ArgumentError, "Unexpected parameter: #{variables_param}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def handle_error_in_development(error)
|
|
51
|
+
logger.error(error.message)
|
|
52
|
+
logger.error(error.backtrace.join("\n"))
|
|
53
|
+
|
|
54
|
+
render json: { errors: [{ message: error.message, backtrace: error.backtrace }], data: {} },
|
|
55
|
+
status: :internal_server_error
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Lighthouse configuration. Every option is optional; the defaults are shown.
|
|
4
|
+
# Reference: https://github.com/Vaz-Innovation/lighthouse-graphql/blob/main/docs/configuration.md
|
|
5
|
+
Lighthouse.configure do |config|
|
|
6
|
+
# Folder the *.graphql schema files are loaded from.
|
|
7
|
+
<% if default_sdl_folder? -%>
|
|
8
|
+
# config.sdl_folder = Rails.root.join('graphql').to_s
|
|
9
|
+
<% else -%>
|
|
10
|
+
config.sdl_folder = Rails.root.join('<%= sdl_folder %>').to_s
|
|
11
|
+
<% end -%>
|
|
12
|
+
|
|
13
|
+
# Where classes named by the schema are looked up, by kind. A Query or Mutation
|
|
14
|
+
# field without a resolving directive resolves by convention: Query.latestPosts
|
|
15
|
+
# to Queries::LatestPosts (a GraphQL::Schema::Resolver), Mutation.publishPost
|
|
16
|
+
# to Mutations::PublishPost (a GraphQL::Schema::Mutation). @builder classes
|
|
17
|
+
# come from :builders and @validator contracts from :validators. Generate them
|
|
18
|
+
# with `rails g lighthouse:query|mutation|builder|validator Name`.
|
|
19
|
+
# config.namespaces = {
|
|
20
|
+
# queries: ['Queries'], mutations: ['Mutations'], builders: ['Builders'], validators: ['Validators']
|
|
21
|
+
# }
|
|
22
|
+
|
|
23
|
+
# Namespaces tried, in order, for models (@all(model: "User")) and for
|
|
24
|
+
# @field(resolver:) on types other than Query and Mutation. Fully-qualified
|
|
25
|
+
# names (containing `::`) are used as-is.
|
|
26
|
+
# config.resolver_namespaces = ['App::Graphql']
|
|
27
|
+
|
|
28
|
+
# Store for @cache and @clearCache.
|
|
29
|
+
# config.cache_store = Rails.cache
|
|
30
|
+
|
|
31
|
+
# Run @create/@update/@createMany/@updateMany in a database transaction.
|
|
32
|
+
# config.transactional_mutations = true
|
|
33
|
+
|
|
34
|
+
# --- For large schemas and high traffic (all off by default) ---------------
|
|
35
|
+
# Cache the built schema document in tmp/cache, keyed by the SDL and
|
|
36
|
+
# everything that shapes it; warm it at deploy with `bin/rails lighthouse:cache`.
|
|
37
|
+
# config.schema_cache = Rails.env.production?
|
|
38
|
+
|
|
39
|
+
# Reload the app (and so the schema) when a .graphql file changes.
|
|
40
|
+
# config.watch_sdl = Rails.env.development?
|
|
41
|
+
|
|
42
|
+
# Automatic Persisted Queries (Apollo's protocol). Add a GET route too, so
|
|
43
|
+
# hashed queries can be cached by CDNs: get "/graphql", to: "graphql#execute"
|
|
44
|
+
# config.persisted_queries = true
|
|
45
|
+
# config.persisted_queries_ttl = 1.week.to_i
|
|
46
|
+
# A manifest of trusted operations from your client build, served by hash;
|
|
47
|
+
# with persisted_queries_only, nothing else may run.
|
|
48
|
+
# config.persisted_queries_manifest = Rails.root.join('persisted-queries.json')
|
|
49
|
+
# config.persisted_queries_only = Rails.env.production?
|
|
50
|
+
|
|
51
|
+
# Called when a directive or resolver swallows an error. The default raises in
|
|
52
|
+
# test and logs otherwise.
|
|
53
|
+
# config.on_error = ->(error, _context) { Rails.error.report(error) }
|
|
54
|
+
|
|
55
|
+
# Authorization seams used by @guard and the @can* directives (Pundit by default).
|
|
56
|
+
# config.authenticated_user = ->(ctx) { ctx[:current_user] }
|
|
57
|
+
# config.policy_user = ->(ctx) { ctx[:pundit_user] || ctx[:current_user] }
|
|
58
|
+
# config.authorizer = ->(user, record, ability, args = []) { Pundit.policy!(user, record).public_send("#{ability}?", *args) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Register app-defined directives and federation reference resolvers inside
|
|
62
|
+
# to_prepare, so they reference reloadable constants and run before the schema
|
|
63
|
+
# is built. See docs/custom-directives.md and docs/federation.md.
|
|
64
|
+
# Rails.application.config.to_prepare do
|
|
65
|
+
# Lighthouse::DirectiveRegistry.register(Directives::Upcase)
|
|
66
|
+
# Lighthouse::ReferenceResolver.register('User') { |ref, _ctx| User.find_by(id: ref['id']) }
|
|
67
|
+
# end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Starter schema generated by `rails generate lighthouse:install`. It assumes a
|
|
2
|
+
# User model with `name` and `email` columns — replace it with your own types.
|
|
3
|
+
# Directives reference: https://github.com/Vaz-Innovation/lighthouse-graphql/blob/main/docs/directives.md
|
|
4
|
+
|
|
5
|
+
"Indicates what fields are available at the top level of a query operation."
|
|
6
|
+
type Query {
|
|
7
|
+
"Find a single user by id."
|
|
8
|
+
user(id: ID! @eq): User @find
|
|
9
|
+
|
|
10
|
+
"List users, a page at a time (10 by default; pass first: and page:)."
|
|
11
|
+
users: [User!]! @paginate(defaultCount: 10)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
"Account of a person who uses this application."
|
|
15
|
+
type User {
|
|
16
|
+
id: ID!
|
|
17
|
+
name: String
|
|
18
|
+
email: String
|
|
19
|
+
}
|