lighthouse-graphql 0.5.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 +118 -1
- data/README.md +24 -16
- data/docs/caching.md +4 -3
- data/docs/configuration.md +1 -21
- data/docs/directives.md +14 -7
- data/docs/generators.md +41 -39
- data/docs/getting-started.md +88 -51
- data/docs/performance.md +2 -2
- data/docs/relationships.md +5 -4
- data/docs/relay.md +63 -0
- data/docs/resolvers.md +90 -37
- data/docs/security.md +64 -16
- data/docs/subscriptions.md +16 -9
- data/docs/types.md +16 -17
- data/docs/uploads.md +41 -14
- data/lib/generators/lighthouse/base_classes.rb +9 -14
- data/lib/generators/lighthouse/install/graphql_ruby_install.rb +113 -0
- data/lib/generators/lighthouse/install/install_generator.rb +112 -91
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +3 -9
- data/lib/lighthouse/caching.rb +9 -3
- data/lib/lighthouse/config.rb +6 -23
- data/lib/lighthouse/controller.rb +28 -5
- data/lib/lighthouse/directive_resolvers/argument_transforms_resolver.rb +18 -18
- data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +1 -1
- data/lib/lighthouse/directive_resolvers/global_id_resolver.rb +3 -1
- data/lib/lighthouse/directive_resolvers/throttle_resolver.rb +85 -26
- data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +12 -6
- data/lib/lighthouse/directives/arguments/more_filters.rb +1 -1
- data/lib/lighthouse/field_resolver_stack.rb +9 -6
- data/lib/lighthouse/global_id.rb +29 -3
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/lib/lighthouse/graphql_ruby_patches.rb +45 -0
- data/lib/lighthouse/rb_lighthouse.rb +1 -0
- data/lib/lighthouse/resolver_bases.rb +18 -4
- data/lib/lighthouse/resolver_dispatch.rb +16 -1
- data/lib/lighthouse/root_types.rb +117 -0
- data/lib/lighthouse/schema_generator.rb +4 -3
- data/lib/lighthouse/upload.rb +4 -2
- data/lib/lighthouse/upload_store.rb +39 -14
- data/sig/lighthouse.rbs +0 -10
- metadata +3 -4
- data/lib/generators/lighthouse/install/templates/schema.rb.tt +0 -29
- data/lib/generators/lighthouse/mutation_generator.rb +0 -31
- data/lib/generators/lighthouse/query_generator.rb +0 -30
data/docs/security.md
CHANGED
|
@@ -10,20 +10,24 @@ Lighthouse works the same way on top of webonyx's validation rules.
|
|
|
10
10
|
|
|
11
11
|
## Setting the limits
|
|
12
12
|
|
|
13
|
-
The limits are graphql-ruby's own schema settings. Set them
|
|
14
|
-
|
|
13
|
+
The limits are graphql-ruby's own schema settings. Set them in the body of the
|
|
14
|
+
schema class, as in any graphql-ruby schema (graphql-ruby's install writes
|
|
15
|
+
`max_depth(15)` there):
|
|
15
16
|
|
|
16
17
|
```ruby
|
|
17
18
|
# app/graphql/app_schema.rb
|
|
18
|
-
AppSchema
|
|
19
|
+
class AppSchema < Lighthouse::GraphQL::RbLightHouse.get_schema(options: { base_types: { object: Types::BaseObject } })
|
|
20
|
+
use GraphQL::Dataloader
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
max_complexity(300) # refuse queries that score over 300
|
|
23
|
+
max_depth(12) # refuse queries nested deeper than 12 fields
|
|
24
|
+
disable_introspection_entry_points if Rails.env.production?
|
|
25
|
+
end
|
|
23
26
|
```
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
`
|
|
28
|
+
A schema assigned to a constant (`AppSchema = get_schema(...)`) takes them as calls
|
|
29
|
+
on it (`AppSchema.max_complexity(300)`). You can also set them on a base schema class
|
|
30
|
+
you hand to `get_schema` with `options: { base_schema: MyBaseSchema }`.
|
|
27
31
|
|
|
28
32
|
A query over the limit isn't executed. Its response holds only the error:
|
|
29
33
|
|
|
@@ -211,17 +215,61 @@ GraphQL::Analysis.analyze_query(query, [GraphQL::Analysis::QueryComplexity]).fir
|
|
|
211
215
|
|
|
212
216
|
## Rate limiting
|
|
213
217
|
|
|
214
|
-
[
|
|
215
|
-
|
|
218
|
+
Rails' own [`rate_limit`](https://api.rubyonrails.org/classes/ActionController/RateLimiting/ClassMethods.html)
|
|
219
|
+
limits the endpoint: every operation counts, and past the limit Rails answers 429.
|
|
220
|
+
[`@throttle`](directives.md#throttle) is the same limit on one field — a login, a search,
|
|
221
|
+
an expensive mutation — beside it:
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
class GraphqlController < ApplicationController
|
|
225
|
+
include Lighthouse::GraphQL::Controller
|
|
226
|
+
|
|
227
|
+
lighthouse_schema AppSchema
|
|
228
|
+
rate_limit to: 600, within: 1.minute, name: 'endpoint'
|
|
229
|
+
|
|
230
|
+
private
|
|
231
|
+
|
|
232
|
+
def too_many_requests
|
|
233
|
+
render json: { errors: [{ message: 'Too many requests. Try again later.' }] }, status: :too_many_requests
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
```
|
|
216
237
|
|
|
217
238
|
```graphql
|
|
218
239
|
type Mutation {
|
|
219
|
-
login(email: String!, password: String!): User @throttle(maxAttempts: 5
|
|
240
|
+
login(email: String!, password: String!): User @throttle(maxAttempts: 5)
|
|
241
|
+
createTag(name: String!): Tag! @throttle(maxAttempts: 20, by: "current_user.id", with: "too_many_requests") @guard @create
|
|
220
242
|
}
|
|
221
243
|
```
|
|
222
244
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
245
|
+
`@throttle` counts as `rate_limit` does, under its key (`rate-limit:<scope>:<name>:<by>`):
|
|
246
|
+
|
|
247
|
+
- **Where.** In the controller's `cache_store`: `config.action_controller.cache_store`,
|
|
248
|
+
or `Rails.cache` when the app sets none (a controller can set its own with
|
|
249
|
+
`self.cache_store =`). Use a store the app servers share (Redis, Solid Cache) in
|
|
250
|
+
production; `:null_store` counts nothing.
|
|
251
|
+
- **Who.** The client's IP, as `rate_limit`'s default. `by:` reads another value off the
|
|
252
|
+
context, as `@inject` does (`by: "current_user.id"`); when it is `nil`, as for a guest,
|
|
253
|
+
those calls count together, as in Rails.
|
|
254
|
+
- **What counts together.** Each field counts under its own name (`Query.login`), apart
|
|
255
|
+
from the endpoint's `rate_limit` (`endpoint`). Give fields, or a field and a
|
|
256
|
+
controller's `rate_limit`, the same `name:` to count them together; `prefix:` is
|
|
257
|
+
`rate_limit`'s `scope:` (the controller's path by default).
|
|
258
|
+
- **Past the limit.** As `rate_limit`'s default, `ActionController::TooManyRequests` is
|
|
259
|
+
raised, and Rails answers the request with a 429 (in development too); on Rails 7.2,
|
|
260
|
+
which has no such error, `head :too_many_requests`. With `with:`, the controller's
|
|
261
|
+
method runs instead, as `rate_limit`'s `with:`. If it renders or redirects, the request
|
|
262
|
+
ends with that response and the operation stops there, as when a `before_action`
|
|
263
|
+
renders. If it returns, the call goes through. If it raises a `GraphQL::ExecutionError`,
|
|
264
|
+
only that field fails, and the rest of the operation is answered:
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
def rate_limited = raise(GraphQL::ExecutionError, 'Too many requests. Try again later.')
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Without Action Pack, the field fails (`Rate limit for Query.x exceeded. Try again later.`).
|
|
271
|
+
- **Instrumentation.** Each refused call is a `rate_limit.action_controller` event, with
|
|
272
|
+
`rate_limit`'s payload (`request`, `count`, `to`, `within`, `by`, `name`, `scope`,
|
|
273
|
+
`cache_key`).
|
|
274
|
+
|
|
275
|
+
A refused call never reaches `@guard` or the resolver.
|
data/docs/subscriptions.md
CHANGED
|
@@ -38,18 +38,24 @@ end
|
|
|
38
38
|
|
|
39
39
|
graphql-ruby delivers subscriptions through a *backend* that stores who subscribed
|
|
40
40
|
and sends the updates: ActionCable (bundled), or a gem such as Pusher or Ably.
|
|
41
|
-
Lighthouse needs none of its own; give the schema one
|
|
42
|
-
|
|
41
|
+
Lighthouse needs none of its own; give the schema one with `use`, in the schema
|
|
42
|
+
class body, as in any graphql-ruby schema:
|
|
43
43
|
|
|
44
44
|
```ruby
|
|
45
45
|
# app/graphql/app_schema.rb
|
|
46
|
-
AppSchema
|
|
47
|
-
options: {
|
|
46
|
+
class AppSchema < Lighthouse::GraphQL::RbLightHouse.get_schema(
|
|
47
|
+
options: { base_types: { subscription: Subscriptions::BaseSubscription } } # and the other bases
|
|
48
48
|
)
|
|
49
|
+
use GraphQL::Dataloader
|
|
50
|
+
use GraphQL::Subscriptions::ActionCableSubscriptions
|
|
51
|
+
end
|
|
49
52
|
```
|
|
50
53
|
|
|
51
|
-
Options of the backend go
|
|
52
|
-
|
|
54
|
+
Options of the backend go after it (`use GraphQL::Subscriptions::ActionCableSubscriptions, namespace: 'my-app'`).
|
|
55
|
+
A schema assigned to a constant instead (`AppSchema = get_schema(...)`) takes the
|
|
56
|
+
backend through the `using:` option, as `use` does:
|
|
57
|
+
`options: { using: { GraphQL::Subscriptions::ActionCableSubscriptions => {} } }`.
|
|
58
|
+
Either way the schema has a name, which a [queued](#queueing) broadcast finds it by.
|
|
53
59
|
|
|
54
60
|
Then add the channel graphql-ruby documents, which runs the operation and keeps
|
|
55
61
|
the client's subscription ids ([the full example](https://graphql-ruby.org/subscriptions/action_cable_implementation.html)):
|
|
@@ -182,7 +188,7 @@ the base every subscription inherits from, as graphql-ruby's convention puts wha
|
|
|
182
188
|
all classes of a kind share in their base:
|
|
183
189
|
|
|
184
190
|
```ruby
|
|
185
|
-
# app/graphql/subscriptions/base_subscription.rb (rails generate lighthouse:install)
|
|
191
|
+
# app/graphql/subscriptions/base_subscription.rb (rails generate lighthouse:install --subscriptions)
|
|
186
192
|
module Subscriptions
|
|
187
193
|
class BaseSubscription < GraphQL::Schema::Subscription
|
|
188
194
|
include Lighthouse::GraphQL::Subscription
|
|
@@ -411,8 +417,9 @@ end
|
|
|
411
417
|
|
|
412
418
|
The job carries the schema's name, the subscription's name, the trigger arguments,
|
|
413
419
|
the scope and the root; an Active Record model is serialized with GlobalID, any
|
|
414
|
-
other root must be something ActiveJob can serialize. A schema
|
|
415
|
-
`get_schema`
|
|
420
|
+
other root must be something ActiveJob can serialize. A schema class
|
|
421
|
+
(`class AppSchema < ...get_schema(...)`), or a schema assigned to a constant
|
|
422
|
+
(`AppSchema = ...`), has the name; an
|
|
416
423
|
anonymous one raises `Lighthouse::DefinitionError` when a queued broadcast needs
|
|
417
424
|
it. The `activejob` gem is optional: nothing loads it unless a broadcast is queued.
|
|
418
425
|
|
data/docs/types.md
CHANGED
|
@@ -69,17 +69,15 @@ Most schemas need nothing else.
|
|
|
69
69
|
|
|
70
70
|
When the default is not enough, say when the values are Hashes from a search
|
|
71
71
|
service rather than records, write the union's class as graphql-ruby does
|
|
72
|
-
(`rails g graphql:union SearchResult`)
|
|
73
|
-
`Types
|
|
74
|
-
|
|
75
|
-
`graphql_name`:
|
|
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
76
|
|
|
77
77
|
```ruby
|
|
78
78
|
# app/graphql/types/search_result_type.rb
|
|
79
79
|
module Types
|
|
80
80
|
class SearchResultType < BaseUnion
|
|
81
|
-
include Lighthouse::GraphQL::Type
|
|
82
|
-
|
|
83
81
|
def self.resolve_type(object, context)
|
|
84
82
|
return unless object.is_a?(Hash)
|
|
85
83
|
|
|
@@ -96,7 +94,6 @@ An interface's goes in graphql-ruby's `definition_methods`:
|
|
|
96
94
|
module Types
|
|
97
95
|
module AuthoredType
|
|
98
96
|
include BaseInterface
|
|
99
|
-
include Lighthouse::GraphQL::Type
|
|
100
97
|
|
|
101
98
|
definition_methods do
|
|
102
99
|
def resolve_type(object, context)
|
|
@@ -154,15 +151,14 @@ type Query {
|
|
|
154
151
|
}
|
|
155
152
|
```
|
|
156
153
|
|
|
157
|
-
Write the scalar's class with graphql-ruby's `rails g graphql:scalar DateTime
|
|
158
|
-
`Types::<Name>Type`
|
|
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`:
|
|
159
157
|
|
|
160
158
|
```ruby
|
|
161
159
|
# app/graphql/types/date_time_type.rb
|
|
162
160
|
module Types
|
|
163
161
|
class DateTimeType < BaseScalar
|
|
164
|
-
include Lighthouse::GraphQL::Type
|
|
165
|
-
|
|
166
162
|
def self.coerce_input(value, context)
|
|
167
163
|
Time.iso8601(value.to_s)
|
|
168
164
|
rescue ArgumentError
|
|
@@ -194,10 +190,12 @@ class with `coerce_input` and `coerce_result` fails when the schema is built.
|
|
|
194
190
|
## Opting in, and base classes
|
|
195
191
|
|
|
196
192
|
Only a class including `Lighthouse::GraphQL::Type` counts: any other
|
|
197
|
-
`Types::SearchResultType` or `Types::DateTimeType
|
|
198
|
-
|
|
199
|
-
`
|
|
200
|
-
|
|
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`.
|
|
201
199
|
|
|
202
200
|
The classes are looked up in [`namespaces[:types]`](configuration.md#namespaces)
|
|
203
201
|
(default `['Types']`). When the schema's `base_types` name a base for a kind,
|
|
@@ -205,15 +203,16 @@ graphql-ruby builds the SDL's types of that kind from it, and a type class of th
|
|
|
205
203
|
kind must inherit from it, or the schema does not build:
|
|
206
204
|
|
|
207
205
|
```ruby
|
|
208
|
-
AppSchema
|
|
206
|
+
class AppSchema < Lighthouse::GraphQL::RbLightHouse.get_schema(
|
|
209
207
|
options: {
|
|
210
208
|
base_types: {
|
|
211
209
|
object: Types::BaseObject,
|
|
212
210
|
union: Types::BaseUnion,
|
|
213
211
|
interface: Types::BaseInterface,
|
|
214
212
|
scalar: Types::BaseScalar
|
|
215
|
-
# resolver:, mutation:,
|
|
213
|
+
# query_type:, node:, resolver:, mutation:, ... (resolvers.md#handing-the-bases-to-the-schema)
|
|
216
214
|
}
|
|
217
215
|
}
|
|
218
216
|
)
|
|
217
|
+
end
|
|
219
218
|
```
|
data/docs/uploads.md
CHANGED
|
@@ -27,7 +27,9 @@ on `User`, the mutation above attaches it.
|
|
|
27
27
|
|
|
28
28
|
## `@upload`
|
|
29
29
|
|
|
30
|
-
`@upload` stores the file first, and passes along what was stored instead
|
|
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:
|
|
31
33
|
|
|
32
34
|
```graphql
|
|
33
35
|
type Mutation {
|
|
@@ -38,23 +40,42 @@ type Mutation {
|
|
|
38
40
|
}
|
|
39
41
|
```
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
+
```
|
|
45
75
|
|
|
46
76
|
`@upload` runs once `@guard`, the `@can*` family and `@validator` have let the call
|
|
47
77
|
through, so a refused call stores nothing.
|
|
48
78
|
|
|
49
|
-
To store files elsewhere, give [`upload_store`](configuration.md#upload_store) an
|
|
50
|
-
object with `call(file, disk:, path:, public:)`; the argument holds what it returns:
|
|
51
|
-
|
|
52
|
-
```ruby
|
|
53
|
-
Lighthouse.configure do |config|
|
|
54
|
-
config.upload_store = ->(file, path: '/', **) { S3Uploader.put(file, prefix: path) } # returns the key
|
|
55
|
-
end
|
|
56
|
-
```
|
|
57
|
-
|
|
58
79
|
## The request
|
|
59
80
|
|
|
60
81
|
```shell
|
|
@@ -68,6 +89,12 @@ curl localhost:3000/graphql \
|
|
|
68
89
|
A batch sends an array as `operations`, and maps files to `"0.variables.avatar"`,
|
|
69
90
|
`"1.variables.files.0"`, and so on.
|
|
70
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
|
+
|
|
71
98
|
In a request spec:
|
|
72
99
|
|
|
73
100
|
```ruby
|
|
@@ -4,19 +4,18 @@ require 'rails/generators'
|
|
|
4
4
|
require 'lighthouse-graphql'
|
|
5
5
|
|
|
6
6
|
# The base class graphql-ruby's convention puts in each namespace, which the
|
|
7
|
-
# classes Lighthouse generates inherit from
|
|
8
|
-
# Mutations::BaseMutation and Subscriptions::BaseSubscription.
|
|
7
|
+
# classes Lighthouse generates inherit from.
|
|
9
8
|
#
|
|
10
|
-
# graphql-ruby's `graphql:install` writes
|
|
11
|
-
# and `lighthouse:install`
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
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.
|
|
15
14
|
#
|
|
16
15
|
# A base that already exists is never replaced, not even with --force: it holds
|
|
17
16
|
# the app's own code.
|
|
18
17
|
module Lighthouse::Generators::BaseClasses
|
|
19
|
-
NAMES = {
|
|
18
|
+
NAMES = { subscriptions: 'BaseSubscription' }.freeze
|
|
20
19
|
|
|
21
20
|
# The bases Lighthouse writes itself, graphql-ruby having none.
|
|
22
21
|
SOURCES = {
|
|
@@ -39,19 +38,15 @@ module Lighthouse::Generators::BaseClasses
|
|
|
39
38
|
NAMES[kind]
|
|
40
39
|
end
|
|
41
40
|
|
|
42
|
-
# Write the base class of `kind` into `namespace` when
|
|
43
|
-
# it is missing; say how to get one graphql-ruby owns.
|
|
41
|
+
# Write the base class of `kind` into `namespace` when it is missing.
|
|
44
42
|
def write_base_class(kind, namespace)
|
|
45
43
|
return unless base_class_name(kind)
|
|
46
44
|
|
|
47
45
|
path = base_class_path(kind, namespace)
|
|
48
46
|
if File.exist?(File.join(destination_root, path))
|
|
49
47
|
say_status :skip, "#{path} already exists", :yellow
|
|
50
|
-
elsif SOURCES[kind]
|
|
51
|
-
create_file path, ruby_source(namespace.split('::'), SOURCES[kind])
|
|
52
48
|
else
|
|
53
|
-
|
|
54
|
-
"or write #{namespace}::#{base_class_name(kind)} yourself", :red
|
|
49
|
+
create_file path, ruby_source(namespace.split('::'), SOURCES.fetch(kind))
|
|
55
50
|
end
|
|
56
51
|
end
|
|
57
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
|