lighthouse-graphql 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +256 -1
- data/README.md +26 -15
- data/docs/caching.md +4 -3
- data/docs/configuration.md +16 -12
- data/docs/directives.md +294 -4
- data/docs/generators.md +84 -0
- data/docs/getting-started.md +93 -60
- data/docs/performance.md +2 -2
- data/docs/relationships.md +47 -4
- data/docs/relay.md +200 -0
- data/docs/resolvers.md +326 -34
- data/docs/security.md +73 -8
- data/docs/subscriptions.md +16 -9
- data/docs/types.md +218 -0
- data/docs/uploads.md +107 -0
- data/lib/generators/lighthouse/base_classes.rb +27 -49
- data/lib/generators/lighthouse/install/graphql_ruby_install.rb +113 -0
- data/lib/generators/lighthouse/install/install_generator.rb +125 -45
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +10 -8
- data/lib/lighthouse/async_mutation_job.rb +17 -0
- data/lib/lighthouse/caching.rb +9 -3
- data/lib/lighthouse/complexity.rb +5 -4
- data/lib/lighthouse/config.rb +12 -18
- data/lib/lighthouse/controller.rb +53 -12
- 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/cache_resolvers.rb +1 -1
- 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 +59 -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 +136 -0
- data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +87 -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 +56 -6
- data/lib/lighthouse/generated_resolvers.rb +13 -8
- data/lib/lighthouse/global_id.rb +57 -0
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/lib/lighthouse/graphql_ruby_patches.rb +45 -0
- 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 +34 -0
- data/lib/lighthouse/relation_counts.rb +29 -0
- data/lib/lighthouse/resolve_info.rb +80 -0
- data/lib/lighthouse/resolver_bases.rb +25 -4
- data/lib/lighthouse/resolver_dispatch.rb +51 -11
- data/lib/lighthouse/root_types.rb +117 -0
- data/lib/lighthouse/scalar_coercion.rb +72 -0
- data/lib/lighthouse/schema_cache.rb +2 -1
- data/lib/lighthouse/schema_generator.rb +13 -11
- 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 +28 -0
- data/lib/lighthouse/upload_store.rb +57 -0
- data/sig/lighthouse/directives.rbs +3 -2
- data/sig/lighthouse.rbs +10 -2
- metadata +40 -4
- data/lib/generators/lighthouse/install/templates/schema.rb.tt +0 -22
- data/lib/generators/lighthouse/mutation_generator.rb +0 -31
- data/lib/generators/lighthouse/query_generator.rb +0 -30
data/docs/types.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Unions, Interfaces and Scalars
|
|
2
|
+
|
|
3
|
+
The SDL declares every type. Object types resolve their fields through
|
|
4
|
+
directives, the default reader, or a [type class](resolvers.md#type-classes).
|
|
5
|
+
Three more kinds need Ruby to say how their values work:
|
|
6
|
+
|
|
7
|
+
- a **union** or an **interface** needs to know which object type each value is;
|
|
8
|
+
- a custom **scalar** needs to know how to read its values from a query and write
|
|
9
|
+
them to a response.
|
|
10
|
+
|
|
11
|
+
Lighthouse answers both as [PHP Lighthouse](https://lighthouse-php.com/master/the-basics/types.html)
|
|
12
|
+
does by default, and lets you take over with a class, in graphql-ruby's
|
|
13
|
+
`app/graphql/types` structure, or with a directive.
|
|
14
|
+
|
|
15
|
+
## Unions and interfaces
|
|
16
|
+
|
|
17
|
+
```graphql
|
|
18
|
+
type Query {
|
|
19
|
+
search(term: String!): [SearchResult!]!
|
|
20
|
+
latestActivity: [Authored!]!
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
union SearchResult = Post | User
|
|
24
|
+
|
|
25
|
+
interface Authored {
|
|
26
|
+
id: ID!
|
|
27
|
+
author: User!
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type Post implements Authored { id: ID! title: String! author: User! }
|
|
31
|
+
type Comment implements Authored { id: ID! body: String! author: User! }
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
A resolver returns records, of any of the types:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
class Resolvers::Search < Resolvers::BaseResolver
|
|
38
|
+
def resolve(term:)
|
|
39
|
+
Post.where('title LIKE ?', "%#{term}%").to_a + User.where('name LIKE ?', "%#{term}%").to_a
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```graphql
|
|
45
|
+
{
|
|
46
|
+
search(term: "Grace") {
|
|
47
|
+
__typename
|
|
48
|
+
... on Post { title }
|
|
49
|
+
... on User { name }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### The default
|
|
55
|
+
|
|
56
|
+
Each value resolves to the first of these that is one of the union's (or the
|
|
57
|
+
interface's) types:
|
|
58
|
+
|
|
59
|
+
1. a Hash's `__typename` (`{ __typename: 'Movie', title: 'Heat' }`);
|
|
60
|
+
2. the type named after the value's class (a `Post` record is a `Post`), or after
|
|
61
|
+
one of its superclasses, so an `Admin < User` STI record is a `User` when no
|
|
62
|
+
`Admin` type exists;
|
|
63
|
+
3. the type whose [`@model`](directives.md#model) names the value's model. With
|
|
64
|
+
`type Article @model(class: "Post")`, a `Post` record is an `Article`.
|
|
65
|
+
|
|
66
|
+
Most schemas need nothing else.
|
|
67
|
+
|
|
68
|
+
### A type class
|
|
69
|
+
|
|
70
|
+
When the default is not enough, say when the values are Hashes from a search
|
|
71
|
+
service rather than records, write the union's class as graphql-ruby does
|
|
72
|
+
(`rails g graphql:union SearchResult`): `Types::<Name>Type`, with `resolve_type`.
|
|
73
|
+
Its base, `Types::BaseUnion`, includes `Lighthouse::GraphQL::Type` after
|
|
74
|
+
`lighthouse:install` (see [opting in](#opting-in-and-base-classes)). It returns a
|
|
75
|
+
type name, or a type class, which stands for the SDL type of its `graphql_name`:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
# app/graphql/types/search_result_type.rb
|
|
79
|
+
module Types
|
|
80
|
+
class SearchResultType < BaseUnion
|
|
81
|
+
def self.resolve_type(object, context)
|
|
82
|
+
return unless object.is_a?(Hash)
|
|
83
|
+
|
|
84
|
+
object['index'] == 'users' ? Types::UserType : 'Post'
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
An interface's goes in graphql-ruby's `definition_methods`:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
# app/graphql/types/authored_type.rb
|
|
94
|
+
module Types
|
|
95
|
+
module AuthoredType
|
|
96
|
+
include BaseInterface
|
|
97
|
+
|
|
98
|
+
definition_methods do
|
|
99
|
+
def resolve_type(object, context)
|
|
100
|
+
object['index'] == 'comments' ? 'Comment' : 'Post' if object.is_a?(Hash)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`nil` leaves the value to the default, so the class only handles the cases that
|
|
108
|
+
need it. The SDL still declares the members: `possible_types` in the class are
|
|
109
|
+
ignored.
|
|
110
|
+
|
|
111
|
+
### `@union` and `@interface`
|
|
112
|
+
|
|
113
|
+
PHP Lighthouse's directives name the class in the schema instead. It has
|
|
114
|
+
`resolve_type(object, context)` or `call(object, context)`, and is looked up in the
|
|
115
|
+
types namespace (nested names too), then as written:
|
|
116
|
+
|
|
117
|
+
```graphql
|
|
118
|
+
union SearchResult @union(resolveType: "SearchResultResolver") = Post | User
|
|
119
|
+
interface Authored @interface(resolveType: "AuthoredResolver") { id: ID! }
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The directive comes first, then the type class, then the default.
|
|
123
|
+
|
|
124
|
+
### When nothing resolves a value
|
|
125
|
+
|
|
126
|
+
A value no step resolves, or one resolved to a type the union or interface does
|
|
127
|
+
not include, raises `Lighthouse::DefinitionError`. It is a server bug, not a client
|
|
128
|
+
error, and the message names the ways out:
|
|
129
|
+
|
|
130
|
+
```text
|
|
131
|
+
Lighthouse could not tell the type of a Tag in the union SearchResult (Post | User):
|
|
132
|
+
name a type after its class, map one to it with @model(class: "Tag"), or resolve
|
|
133
|
+
it in Types::SearchResultType.resolve_type or with @union(resolveType:).
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
A `@union` or `@interface` naming no class, or a type class of another kind than
|
|
137
|
+
its SDL type (a union class for an interface), fails when the schema is built.
|
|
138
|
+
|
|
139
|
+
## Scalars
|
|
140
|
+
|
|
141
|
+
```graphql
|
|
142
|
+
"An ISO 8601 date and time."
|
|
143
|
+
scalar DateTime
|
|
144
|
+
|
|
145
|
+
type Post {
|
|
146
|
+
createdAt: DateTime!
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
type Query {
|
|
150
|
+
latestActivity(since: DateTime): [Authored!]!
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Write the scalar's class with graphql-ruby's `rails g graphql:scalar DateTime`:
|
|
155
|
+
`Types::<Name>Type`, a `Types::BaseScalar`, which includes `Lighthouse::GraphQL::Type`
|
|
156
|
+
after `lighthouse:install`:
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
# app/graphql/types/date_time_type.rb
|
|
160
|
+
module Types
|
|
161
|
+
class DateTimeType < BaseScalar
|
|
162
|
+
def self.coerce_input(value, context)
|
|
163
|
+
Time.iso8601(value.to_s)
|
|
164
|
+
rescue ArgumentError
|
|
165
|
+
raise GraphQL::CoercionError, "#{value.inspect} is not an ISO 8601 date and time"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def self.coerce_result(value, context)
|
|
169
|
+
value.utc.iso8601
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
- `coerce_input` reads an argument or variable. The resolver gets its result, here a
|
|
176
|
+
`Time`. Raising `GraphQL::CoercionError` (or returning `nil`) makes the argument
|
|
177
|
+
invalid, and the client gets the message.
|
|
178
|
+
- `coerce_result` writes a value a field returns, here `Post#created_at`.
|
|
179
|
+
|
|
180
|
+
`@scalar(class:)`, PHP Lighthouse's directive, names the class in the schema
|
|
181
|
+
instead, looked up in the types namespace, then as written:
|
|
182
|
+
|
|
183
|
+
```graphql
|
|
184
|
+
scalar Money @scalar(class: "Money::GraphQLScalar")
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
A scalar with neither passes its values through unchanged. A `@scalar` naming no
|
|
188
|
+
class with `coerce_input` and `coerce_result` fails when the schema is built.
|
|
189
|
+
|
|
190
|
+
## Opting in, and base classes
|
|
191
|
+
|
|
192
|
+
Only a class including `Lighthouse::GraphQL::Type` counts: any other
|
|
193
|
+
`Types::SearchResultType` or `Types::DateTimeType` is left alone. `lighthouse:install`
|
|
194
|
+
includes the concern in graphql-ruby's `Types::BaseObject`, `BaseInterface`,
|
|
195
|
+
`BaseUnion` and `BaseScalar`, so every type class inheriting from them counts. In an
|
|
196
|
+
app that did not run it, include the concern by hand, in those bases or in each
|
|
197
|
+
class: a subclass of `GraphQL::Schema::Object`, `Union` or `Scalar`, or a module
|
|
198
|
+
including `GraphQL::Schema::Interface`.
|
|
199
|
+
|
|
200
|
+
The classes are looked up in [`namespaces[:types]`](configuration.md#namespaces)
|
|
201
|
+
(default `['Types']`). When the schema's `base_types` name a base for a kind,
|
|
202
|
+
graphql-ruby builds the SDL's types of that kind from it, and a type class of that
|
|
203
|
+
kind must inherit from it, or the schema does not build:
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
class AppSchema < Lighthouse::GraphQL::RbLightHouse.get_schema(
|
|
207
|
+
options: {
|
|
208
|
+
base_types: {
|
|
209
|
+
object: Types::BaseObject,
|
|
210
|
+
union: Types::BaseUnion,
|
|
211
|
+
interface: Types::BaseInterface,
|
|
212
|
+
scalar: Types::BaseScalar
|
|
213
|
+
# query_type:, node:, resolver:, mutation:, ... (resolvers.md#handing-the-bases-to-the-schema)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
end
|
|
218
|
+
```
|
data/docs/uploads.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# File Uploads
|
|
2
|
+
|
|
3
|
+
Clients send files with the [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec),
|
|
4
|
+
which Apollo Upload Client, urql and most GraphQL clients speak: a multipart form with
|
|
5
|
+
the operation as JSON, a map of where each file goes, and the files.
|
|
6
|
+
`Lighthouse::GraphQL::Controller` reads it, so the endpoint needs nothing more.
|
|
7
|
+
|
|
8
|
+
## The `Upload` scalar
|
|
9
|
+
|
|
10
|
+
Declare it, and use it for arguments:
|
|
11
|
+
|
|
12
|
+
```graphql
|
|
13
|
+
"A file sent with a multipart request."
|
|
14
|
+
scalar Upload
|
|
15
|
+
|
|
16
|
+
type Mutation {
|
|
17
|
+
updateAvatar(avatar: Upload!): User! @update @inject(context: "current_user.id", name: "id")
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Lighthouse reads its values itself (`Lighthouse::GraphQL::Upload`): the resolver gets
|
|
22
|
+
the uploaded file, an `ActionDispatch::Http::UploadedFile`. A value that is not a file
|
|
23
|
+
makes the argument invalid. `Upload` is for arguments only, never a field's result.
|
|
24
|
+
|
|
25
|
+
An ActiveStorage attachment takes the file as it is: with `has_one_attached :avatar`
|
|
26
|
+
on `User`, the mutation above attaches it.
|
|
27
|
+
|
|
28
|
+
## `@upload`
|
|
29
|
+
|
|
30
|
+
`@upload` stores the file first, and passes along what was stored instead, as PHP
|
|
31
|
+
Lighthouse's `@upload` does on a Laravel disk. The file goes to your app's
|
|
32
|
+
ActiveStorage, the way the rest of the app stores files:
|
|
33
|
+
|
|
34
|
+
```graphql
|
|
35
|
+
type Mutation {
|
|
36
|
+
updateAvatar(avatar: Upload! @upload(path: "avatars")): User!
|
|
37
|
+
@guard
|
|
38
|
+
@update
|
|
39
|
+
@inject(context: "current_user.id", name: "id")
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
| | PHP Lighthouse (Laravel) | lighthouse-graphql (ActiveStorage) |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| `disk` | a disk of `config/filesystems.php`; `filesystems.default` without it | a service of `config/storage.yml`; `config.active_storage.service` without it |
|
|
46
|
+
| `path` (`"/"`) | the directory the file is stored in | the directory the blob's key starts with (`avatars/<token>`); at `"/"`, ActiveStorage's own key |
|
|
47
|
+
| `public` (`false`) | the file's visibility | must agree with the service's (see below) |
|
|
48
|
+
| the argument holds | the stored path | the blob's **signed id** |
|
|
49
|
+
|
|
50
|
+
The signed id is what an attachment attribute attaches (`user.avatar = signed_id`) and
|
|
51
|
+
`ActiveStorage::Blob.find_signed` finds; the blob keeps the file's name and content
|
|
52
|
+
type. Lists of files are stored one by one, and `null` stays `null`.
|
|
53
|
+
|
|
54
|
+
ActiveStorage makes a whole service public (`public: true` in `config/storage.yml`)
|
|
55
|
+
or private, never one file. So `public:` must agree with the service's, and the schema
|
|
56
|
+
never says a file is private that anyone can read: `@upload(public: true)` on a private
|
|
57
|
+
service fails, and so does `@upload` (`public: false`) on a public one. A service
|
|
58
|
+
`config/storage.yml` does not declare fails too.
|
|
59
|
+
|
|
60
|
+
To store files somewhere else (S3, GCS, Azure or a service of your own), declare an
|
|
61
|
+
ActiveStorage service for it and name it with `disk:`, as a Laravel app declares a disk:
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
# config/storage.yml
|
|
65
|
+
avatars:
|
|
66
|
+
service: S3
|
|
67
|
+
bucket: my-app-avatars
|
|
68
|
+
region: us-east-1
|
|
69
|
+
public: true
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```graphql
|
|
73
|
+
updateAvatar(avatar: Upload! @upload(disk: "avatars", path: "users", public: true)): User!
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`@upload` runs once `@guard`, the `@can*` family and `@validator` have let the call
|
|
77
|
+
through, so a refused call stores nothing.
|
|
78
|
+
|
|
79
|
+
## The request
|
|
80
|
+
|
|
81
|
+
```shell
|
|
82
|
+
curl localhost:3000/graphql \
|
|
83
|
+
-H 'Authorization: Bearer <token>' \
|
|
84
|
+
-F operations='{ "query": "mutation($avatar: Upload!) { updateAvatar(avatar: $avatar) { avatarUrl } }", "variables": { "avatar": null } }' \
|
|
85
|
+
-F map='{ "0": ["variables.avatar"] }' \
|
|
86
|
+
-F 0=@me.png
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
A batch sends an array as `operations`, and maps files to `"0.variables.avatar"`,
|
|
90
|
+
`"1.variables.files.0"`, and so on.
|
|
91
|
+
|
|
92
|
+
A multipart request is also what an HTML form on another site can send. If the GraphQL
|
|
93
|
+
controller authenticates with the session cookie, keep Rails' CSRF protection on it
|
|
94
|
+
(`protect_from_forgery`, on by default in `ActionController::Base`) and have clients send
|
|
95
|
+
the `X-CSRF-Token` header, as PHP Lighthouse has them send `X-Requested-With` for its
|
|
96
|
+
`EnsureXHR` middleware. A controller that authenticates with a token header is not exposed.
|
|
97
|
+
|
|
98
|
+
In a request spec:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
avatar = Rack::Test::UploadedFile.new('spec/fixtures/me.png', 'image/png')
|
|
102
|
+
post '/graphql', params: {
|
|
103
|
+
operations: { query: mutation, variables: { avatar: nil } }.to_json,
|
|
104
|
+
map: { '0' => ['variables.avatar'] }.to_json,
|
|
105
|
+
'0' => avatar
|
|
106
|
+
}
|
|
107
|
+
```
|
|
@@ -3,72 +3,50 @@
|
|
|
3
3
|
require 'rails/generators'
|
|
4
4
|
require 'lighthouse-graphql'
|
|
5
5
|
|
|
6
|
-
# The base class graphql-ruby's convention puts in each namespace
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
6
|
+
# The base class graphql-ruby's convention puts in each namespace, which the
|
|
7
|
+
# classes Lighthouse generates inherit from.
|
|
8
|
+
#
|
|
9
|
+
# graphql-ruby's `graphql:install` writes Resolvers::BaseResolver,
|
|
10
|
+
# Mutations::BaseMutation and every Types::Base*, and `lighthouse:install`
|
|
11
|
+
# builds on them: Lighthouse does not write copies of them. It writes only the
|
|
12
|
+
# base graphql-ruby has none of, Subscriptions::BaseSubscription, which
|
|
13
|
+
# includes Lighthouse's subscription concern.
|
|
12
14
|
#
|
|
13
15
|
# A base that already exists is never replaced, not even with --force: it holds
|
|
14
|
-
# the app's own code
|
|
16
|
+
# the app's own code.
|
|
15
17
|
module Lighthouse::Generators::BaseClasses
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
source: <<~RUBY
|
|
31
|
-
# What every class resolving a Mutation field shares: Lighthouse resolves
|
|
32
|
-
# Mutation.publishPost to Mutations::PublishPost by convention, and every such
|
|
33
|
-
# class inherits from this one. The SDL declares the arguments, so it is a
|
|
34
|
-
# plain GraphQL::Schema::Mutation; the GraphQL::Schema::RelayClassicMutation
|
|
35
|
-
# graphql-ruby's `graphql:install` writes (one `input:` argument) works too.
|
|
36
|
-
class BaseMutation < GraphQL::Schema::Mutation
|
|
37
|
-
end
|
|
38
|
-
RUBY
|
|
39
|
-
},
|
|
40
|
-
subscriptions: {
|
|
41
|
-
name: 'BaseSubscription',
|
|
42
|
-
source: <<~RUBY
|
|
43
|
-
# What every class resolving a Subscription field shares. The concern is PHP
|
|
44
|
-
# Lighthouse's model: every subscriber of a field gets every broadcast, and the
|
|
45
|
-
# class's `filter?` decides who receives it. A subclass that defines
|
|
46
|
-
# graphql-ruby's own `authorized?` or `update` keeps them, and `topic_by
|
|
47
|
-
# :arguments` brings back graphql-ruby's exact-argument matching.
|
|
48
|
-
class BaseSubscription < GraphQL::Schema::Subscription
|
|
49
|
-
include Lighthouse::GraphQL::Subscription
|
|
50
|
-
end
|
|
51
|
-
RUBY
|
|
52
|
-
}
|
|
18
|
+
NAMES = { subscriptions: 'BaseSubscription' }.freeze
|
|
19
|
+
|
|
20
|
+
# The bases Lighthouse writes itself, graphql-ruby having none.
|
|
21
|
+
SOURCES = {
|
|
22
|
+
subscriptions: <<~RUBY
|
|
23
|
+
# What every class resolving a Subscription field shares. The concern is PHP
|
|
24
|
+
# Lighthouse's model: every subscriber of a field gets every broadcast, and the
|
|
25
|
+
# class's `filter?` decides who receives it. A subclass that defines
|
|
26
|
+
# graphql-ruby's own `authorized?` or `update` keeps them, and `topic_by
|
|
27
|
+
# :arguments` brings back graphql-ruby's exact-argument matching.
|
|
28
|
+
class BaseSubscription < GraphQL::Schema::Subscription
|
|
29
|
+
include Lighthouse::GraphQL::Subscription
|
|
30
|
+
end
|
|
31
|
+
RUBY
|
|
53
32
|
}.freeze
|
|
54
33
|
|
|
55
34
|
private
|
|
56
35
|
|
|
57
36
|
# The base class name for `kind` (`BaseResolver`), or nil for a kind without one.
|
|
58
37
|
def base_class_name(kind)
|
|
59
|
-
|
|
38
|
+
NAMES[kind]
|
|
60
39
|
end
|
|
61
40
|
|
|
62
|
-
# Write the base class of `kind` into `namespace
|
|
41
|
+
# Write the base class of `kind` into `namespace` when it is missing.
|
|
63
42
|
def write_base_class(kind, namespace)
|
|
64
|
-
|
|
65
|
-
return unless base
|
|
43
|
+
return unless base_class_name(kind)
|
|
66
44
|
|
|
67
45
|
path = base_class_path(kind, namespace)
|
|
68
46
|
if File.exist?(File.join(destination_root, path))
|
|
69
47
|
say_status :skip, "#{path} already exists", :yellow
|
|
70
48
|
else
|
|
71
|
-
create_file path, ruby_source(namespace.split('::'),
|
|
49
|
+
create_file path, ruby_source(namespace.split('::'), SOURCES.fetch(kind))
|
|
72
50
|
end
|
|
73
51
|
end
|
|
74
52
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# What graphql-ruby's `graphql:install` wrote in an app, as lighthouse:install
|
|
4
|
+
# reads it: where its schema class is (under app/, beside its types: --directory,
|
|
5
|
+
# --schema), which of its classes exist (--relay, --skip-mutation-root-type), and
|
|
6
|
+
# the parts of its templates Lighthouse takes out.
|
|
7
|
+
module Lighthouse::Generators::GraphqlRubyInstall
|
|
8
|
+
# graphql-ruby's code-first root types and Node interface, by base_types key (RootTypes).
|
|
9
|
+
ROOT_TYPES = { query_type: 'QueryType', mutation_type: 'MutationType', subscription_type: 'SubscriptionType', node: 'NodeType' }.freeze
|
|
10
|
+
|
|
11
|
+
# graphql-ruby's base types, by base_types key.
|
|
12
|
+
TYPE_BASES = {
|
|
13
|
+
object: 'BaseObject', interface: 'BaseInterface', union: 'BaseUnion', scalar: 'BaseScalar',
|
|
14
|
+
enum: 'BaseEnum', input_object: 'BaseInputObject'
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
# Where Lighthouse::GraphQL::Type goes in graphql-ruby's base types: after the
|
|
18
|
+
# class line, or the interface's own include.
|
|
19
|
+
TYPE_CONCERN_ANCHORS = {
|
|
20
|
+
'base_object' => /^([ \t]*)class BaseObject < GraphQL::Schema::Object\n/,
|
|
21
|
+
'base_union' => /^([ \t]*)class BaseUnion < GraphQL::Schema::Union\n/,
|
|
22
|
+
'base_scalar' => /^([ \t]*)class BaseScalar < GraphQL::Schema::Scalar\n/,
|
|
23
|
+
'base_interface' => /^([ \t]*)include GraphQL::Schema::Interface\n/
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
# The example field graphql-ruby's install puts in each root type.
|
|
27
|
+
TEST_FIELD = /\n+[ \t]*# TODO: remove me\n[ \t]*field :test_field,.*?\n[ \t]*def test_field\n.*?\n[ \t]*end\n/m
|
|
28
|
+
|
|
29
|
+
# The `resolve_type` stub graphql-ruby's install puts in the schema class.
|
|
30
|
+
RESOLVE_TYPE_STUB = /
|
|
31
|
+
\n+[ \t]*\#\ Union\ and\ Interface\ Resolution\n
|
|
32
|
+
[ \t]*def\ self\.resolve_type\(.*?\)\n
|
|
33
|
+
(?:[ \t]*\#.*\n)*
|
|
34
|
+
[ \t]*raise\(GraphQL::RequiredImplementationMissingError\)\n
|
|
35
|
+
[ \t]*end\n
|
|
36
|
+
/x
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# The file of graphql-ruby's schema class under app/ (--schema picks one by name), or nil.
|
|
41
|
+
def schema_path
|
|
42
|
+
@schema_path ||= Dir[File.join(destination_root, 'app', '**', '*.rb')].filter_map do |file|
|
|
43
|
+
name = schema_class_name(File.read(file))
|
|
44
|
+
file.delete_prefix("#{destination_root}/") if name && (options[:schema].nil? || name.demodulize == options[:schema].camelize)
|
|
45
|
+
end.first
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def schema_class_name(source)
|
|
49
|
+
source[/^[ \t]*class (\S+) < (?:GraphQL::Schema|Lighthouse::GraphQL::RbLightHouse\.get_schema)\b/, 1] ||
|
|
50
|
+
source[/^[ \t]*(\S+) = Lighthouse::GraphQL::RbLightHouse\.get_schema\b/, 1]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def schema_name
|
|
54
|
+
schema_class_name(read(schema_path)).demodulize
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def lighthouse?(source)
|
|
58
|
+
source.include?('Lighthouse::GraphQL::RbLightHouse.get_schema')
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# graphql-ruby's install: its defaults, an API app's smaller stack, the name --schema gives.
|
|
62
|
+
def graphql_install_options
|
|
63
|
+
{ schema: options[:schema], api: api_only? || nil }.compact
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def api_only?
|
|
67
|
+
exist?('config/application.rb') && read('config/application.rb').match?(/^\s*config\.api_only\s*=\s*true/)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# graphql-ruby writes its types beside the schema class (--directory).
|
|
71
|
+
def directory
|
|
72
|
+
File.dirname(schema_path)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def types_path(file)
|
|
76
|
+
File.join(directory, 'types', file)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Lighthouse's base_types: every class graphql-ruby's install wrote, found
|
|
80
|
+
# where it writes it, as [key, constant] pairs.
|
|
81
|
+
def base_types
|
|
82
|
+
@base_types ||= type_classes(ROOT_TYPES) + resolver_classes + type_classes(TYPE_BASES)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def type_classes(names)
|
|
86
|
+
names.filter_map { |key, name| [key, "Types::#{name}"] if exist?(types_path("#{name.underscore}.rb")) }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def resolver_classes
|
|
90
|
+
classes = { resolver: %w[resolvers BaseResolver], mutation: %w[mutations BaseMutation] }
|
|
91
|
+
classes[:subscription] = %w[subscriptions BaseSubscription] if subscription_base?
|
|
92
|
+
classes.filter_map do |key, (folder, name)|
|
|
93
|
+
[key, "#{folder.camelize}::#{name}"] if exist?(File.join(directory, folder, "#{name.underscore}.rb"))
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def subscription_base?
|
|
98
|
+
options[:subscriptions] || exist?(File.join(directory, 'subscriptions', 'base_subscription.rb'))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def graphql_ruby_controller?(source)
|
|
102
|
+
source.include?('def prepare_variables(variables_param)') && source.include?('def handle_error_in_development(e)') &&
|
|
103
|
+
source.include?("#{schema_name}.execute(query, variables: variables, context: context, operation_name: operation_name)")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def read(path)
|
|
107
|
+
File.read(File.join(destination_root, path))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def exist?(path)
|
|
111
|
+
File.exist?(File.join(destination_root, path))
|
|
112
|
+
end
|
|
113
|
+
end
|