lighthouse-graphql 0.5.0 → 0.7.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 +212 -1
- data/README.md +38 -22
- data/docs/best-practices.md +6 -4
- data/docs/caching.md +4 -3
- data/docs/configuration.md +20 -26
- data/docs/custom-directives.md +2 -1
- data/docs/directives.md +79 -14
- data/docs/federation.md +1 -1
- data/docs/filtering-and-ordering.md +330 -30
- data/docs/generators.md +42 -40
- data/docs/getting-started.md +128 -58
- data/docs/motivation.md +3 -2
- data/docs/performance.md +5 -4
- data/docs/relationships.md +7 -5
- data/docs/relay.md +63 -0
- data/docs/resolvers.md +92 -39
- data/docs/schema-organisation.md +177 -0
- 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 +125 -99
- data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +2 -1
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +14 -13
- data/lib/lighthouse/ast_builder.rb +124 -0
- data/lib/lighthouse/caching.rb +9 -3
- data/lib/lighthouse/config.rb +27 -25
- data/lib/lighthouse/contracts/arg_manipulator.rb +9 -5
- data/lib/lighthouse/contracts/field_builder.rb +13 -0
- data/lib/lighthouse/controller.rb +28 -5
- data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +1 -2
- data/lib/lighthouse/directive_resolvers/all_resolver.rb +0 -2
- data/lib/lighthouse/directive_resolvers/argument_transforms_resolver.rb +18 -18
- data/lib/lighthouse/directive_resolvers/association_resolvers.rb +9 -6
- data/lib/lighthouse/directive_resolvers/base.rb +6 -37
- data/lib/lighthouse/directive_resolvers/builder_applier.rb +21 -6
- data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +1 -1
- data/lib/lighthouse/directive_resolvers/count_resolver.rb +0 -1
- data/lib/lighthouse/directive_resolvers/first_resolver.rb +0 -2
- data/lib/lighthouse/directive_resolvers/global_id_resolver.rb +3 -1
- data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +2 -3
- 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/directives/arguments/order_by.rb +190 -26
- data/lib/lighthouse/directives/arguments/where_conditions.rb +25 -0
- data/lib/lighthouse/directives/arguments/where_conditions_base.rb +84 -0
- data/lib/lighthouse/directives/arguments/where_has_conditions.rb +56 -0
- data/lib/lighthouse/directives/definitions.rb +0 -1
- data/lib/lighthouse/directives/generated_types.rb +103 -0
- data/lib/lighthouse/directives/registry.rb +39 -10
- data/lib/lighthouse/document_ast.rb +111 -0
- 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/ide_helper.rb +80 -23
- data/lib/lighthouse/namespaces.rb +1 -0
- data/lib/lighthouse/order_by/types.rb +137 -0
- data/lib/lighthouse/railtie.rb +9 -2
- data/lib/lighthouse/rb_lighthouse.rb +16 -5
- data/lib/lighthouse/resolve_info.rb +5 -5
- 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_cache.rb +8 -4
- data/lib/lighthouse/schema_factory.rb +6 -9
- data/lib/lighthouse/schema_generator.rb +8 -18
- data/lib/lighthouse/schema_stitcher.rb +64 -0
- data/lib/lighthouse/support/naming.rb +0 -12
- data/lib/lighthouse/upload.rb +4 -2
- data/lib/lighthouse/upload_store.rb +39 -14
- data/lib/lighthouse/where_conditions/columns.rb +36 -0
- data/lib/lighthouse/where_conditions/handler.rb +152 -0
- data/lib/lighthouse/where_conditions/json_column.rb +99 -0
- data/lib/lighthouse/where_conditions/operator_map.rb +1 -4
- data/lib/lighthouse/where_conditions/types.rb +110 -0
- data/sig/lighthouse/directives.rbs +16 -3
- data/sig/lighthouse/graphql.rbs +48 -4
- data/sig/lighthouse.rbs +8 -12
- metadata +17 -7
- 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/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +0 -170
- data/lib/lighthouse/directives/where_conditions_directive.rb +0 -244
- data/lib/lighthouse/sdl_loader.rb +0 -22
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
|
|
@@ -4,50 +4,66 @@ require 'rails/generators'
|
|
|
4
4
|
require 'generators/graphql/install_generator'
|
|
5
5
|
require 'lighthouse-graphql'
|
|
6
6
|
require_relative '../base_classes'
|
|
7
|
+
require_relative 'graphql_ruby_install'
|
|
7
8
|
|
|
8
|
-
# `rails generate lighthouse:install` —
|
|
9
|
-
#
|
|
10
|
-
# --
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# Lighthouse then puts its own over what graphql-ruby wrote in this run:
|
|
9
|
+
# `rails generate lighthouse:install` — puts Lighthouse on top of graphql-ruby's
|
|
10
|
+
# own install (`rails g graphql:install`, with whichever of its options the app
|
|
11
|
+
# chose: --relay, --batch, --skip-mutation-root-type, --schema, --directory,
|
|
12
|
+
# ...). When the app has none, it runs graphql:install first, with its
|
|
13
|
+
# defaults. It then adapts what graphql-ruby wrote, instead of writing its own:
|
|
14
14
|
#
|
|
15
|
-
# - the schema class
|
|
16
|
-
#
|
|
17
|
-
# -
|
|
18
|
-
#
|
|
19
|
-
#
|
|
15
|
+
# - the schema class (`class AppSchema < GraphQL::Schema`) becomes Lighthouse's,
|
|
16
|
+
# built from the SDL on everything graphql-ruby wrote: its base types, base
|
|
17
|
+
# resolver and base mutation, and its code-first Types::QueryType /
|
|
18
|
+
# MutationType, which the SDL's root types extend, so their `field`s (Relay's
|
|
19
|
+
# `node` / `nodes`, what `graphql:mutation` adds) stay. Its Relay
|
|
20
|
+
# Types::NodeType is the SDL's Node. Everything else in the class is
|
|
21
|
+
# graphql-ruby's and stays, but the `resolve_type` stub (Lighthouse resolves
|
|
22
|
+
# types) and the root types it is built on;
|
|
23
|
+
# - the example `test_field` goes from the root types;
|
|
24
|
+
# - graphql-ruby's base object, interface, union and scalar include
|
|
25
|
+
# Lighthouse::GraphQL::Type, so every type class (Types::QueryType,
|
|
26
|
+
# Types::NodeType, what `graphql:object` writes) is Lighthouse's;
|
|
27
|
+
# - with --batch, the schema uses GraphQL::Dataloader too, which Lighthouse's
|
|
28
|
+
# directives batch through;
|
|
29
|
+
# - graphql-ruby's controller, as its install wrote it, becomes one with
|
|
30
|
+
# Lighthouse::GraphQL::Controller.
|
|
20
31
|
#
|
|
21
|
-
#
|
|
22
|
-
# and
|
|
23
|
-
#
|
|
24
|
-
# it), so an app already serving a graphql-ruby schema keeps doing so until it
|
|
25
|
-
# is pointed at ours.
|
|
32
|
+
# It adds the initializer, a starter SDL file and the graphql-config file IDEs
|
|
33
|
+
# read, and with --subscriptions, Subscriptions::BaseSubscription. Rerunning it
|
|
34
|
+
# skips what is in place.
|
|
26
35
|
class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
|
|
27
36
|
include Lighthouse::Generators::BaseClasses
|
|
37
|
+
include Lighthouse::Generators::GraphqlRubyInstall
|
|
28
38
|
|
|
29
39
|
CONTROLLER = 'app/controllers/graphql_controller.rb'
|
|
30
|
-
ROOT_TYPES = %w[app/graphql/types/query_type.rb app/graphql/types/mutation_type.rb].freeze
|
|
31
|
-
BASE_MUTATION = 'app/graphql/mutations/base_mutation.rb'
|
|
32
40
|
|
|
33
41
|
source_root File.expand_path('templates', __dir__)
|
|
34
42
|
|
|
35
|
-
desc
|
|
43
|
+
desc "Install Lighthouse on graphql-ruby's install: schema class, base classes, controller, initializer, starter SDL and IDE config."
|
|
36
44
|
|
|
37
|
-
class_option :schema, type: :string,
|
|
38
|
-
desc:
|
|
39
|
-
class_option :
|
|
40
|
-
|
|
45
|
+
class_option :schema, type: :string,
|
|
46
|
+
desc: "The schema class to adopt; for graphql:install, when it runs, its name (default: graphql-ruby's)"
|
|
47
|
+
class_option :schema_path, type: :string, default: 'graphql/schema.graphql',
|
|
48
|
+
desc: "The schema's entrypoint, relative to the app root; it #imports the other .graphql files"
|
|
49
|
+
class_option :subscriptions, type: :boolean, default: false,
|
|
50
|
+
desc: 'Add Subscriptions::BaseSubscription, the base of the classes Subscription fields resolve to'
|
|
41
51
|
class_option :skip_controller, type: :boolean, default: false,
|
|
42
|
-
desc: '
|
|
52
|
+
desc: "Leave graphql-ruby's GraphqlController as it is"
|
|
43
53
|
class_option :skip_ide, type: :boolean, default: false,
|
|
44
54
|
desc: 'Skip graphql.config.yml and the .gitignore entries for the IDE helper files'
|
|
45
55
|
|
|
46
|
-
# graphql-ruby's install
|
|
47
|
-
# replaces is noted, so a file the app had before is never touched.
|
|
56
|
+
# graphql-ruby's install: the app's, or one now, with graphql-ruby's defaults.
|
|
48
57
|
def install_graphql_ruby
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
return if schema_path
|
|
59
|
+
|
|
60
|
+
say_status :invoke, 'graphql:install (no graphql-ruby schema found)', :blue
|
|
61
|
+
invoke 'graphql:install', [], graphql_install_options
|
|
62
|
+
@schema_path = nil
|
|
63
|
+
return if schema_path
|
|
64
|
+
|
|
65
|
+
raise Thor::Error, "No graphql-ruby schema class (class #{options[:schema] || 'AppSchema'} < GraphQL::Schema) under app/: " \
|
|
66
|
+
'run `bin/rails g graphql:install` first.'
|
|
51
67
|
end
|
|
52
68
|
|
|
53
69
|
def create_initializer
|
|
@@ -55,46 +71,68 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
|
|
|
55
71
|
end
|
|
56
72
|
|
|
57
73
|
def create_starter_schema
|
|
58
|
-
if
|
|
59
|
-
say_status :skip, "#{
|
|
74
|
+
if File.exist?(File.join(destination_root, sdl_schema_path))
|
|
75
|
+
say_status :skip, "#{sdl_schema_path} exists", :yellow
|
|
60
76
|
return
|
|
61
77
|
end
|
|
62
78
|
|
|
63
|
-
template 'schema.graphql.tt',
|
|
79
|
+
template 'schema.graphql.tt', sdl_schema_path
|
|
64
80
|
end
|
|
65
81
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
template 'schema.rb.tt', schema_path, force: true
|
|
82
|
+
# Subscriptions::BaseSubscription, which graphql-ruby has none of (see BaseClasses).
|
|
83
|
+
def create_base_subscription
|
|
84
|
+
write_base_class(:subscriptions, Lighthouse::Config.namespaces[:subscriptions].first) if options[:subscriptions]
|
|
70
85
|
end
|
|
71
86
|
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
87
|
+
# graphql-ruby's schema class, built by Lighthouse from the SDL on its classes.
|
|
88
|
+
def adopt_schema_class
|
|
89
|
+
source = read(schema_path)
|
|
90
|
+
if lighthouse?(source)
|
|
91
|
+
say_status :skip, "#{schema_path} is Lighthouse's already", :yellow
|
|
92
|
+
return
|
|
93
|
+
end
|
|
77
94
|
|
|
78
|
-
|
|
95
|
+
declaration = /^([ \t]*)class (\S+) < GraphQL::Schema[ \t]*\n/
|
|
96
|
+
indent, name = source.match(declaration).captures
|
|
97
|
+
gsub_file schema_path, declaration, "#{indent}#{schema_comment(indent)}#{indent}class #{name} < #{schema_superclass(indent)}\n"
|
|
98
|
+
gsub_file schema_path, /^[ \t]*(query|mutation|subscription)\(Types::(Query|Mutation|Subscription)Type\)\n/, ''
|
|
99
|
+
gsub_file schema_path, /^(#{indent}\)\n)\n+/, '\\1'
|
|
100
|
+
gsub_file schema_path, RESOLVE_TYPE_STUB, "\n"
|
|
101
|
+
add_dataloader if source.include?('use GraphQL::Batch') && source.exclude?('use GraphQL::Dataloader')
|
|
79
102
|
end
|
|
80
103
|
|
|
81
|
-
# The
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
gsub_file BASE_MUTATION, /^\s*input_object_class .*\n/, ''
|
|
104
|
+
# The example field goes; the root types' own fields stay.
|
|
105
|
+
def adapt_root_types
|
|
106
|
+
%w[query_type mutation_type subscription_type].each do |name|
|
|
107
|
+
path = types_path("#{name}.rb")
|
|
108
|
+
gsub_file path, TEST_FIELD, "\n" if exist?(path) && read(path).match?(TEST_FIELD)
|
|
109
|
+
end
|
|
88
110
|
end
|
|
89
111
|
|
|
90
|
-
#
|
|
91
|
-
|
|
92
|
-
|
|
112
|
+
# Every type class is Lighthouse's: graphql-ruby's base object, interface,
|
|
113
|
+
# union and scalar include Lighthouse::GraphQL::Type, so the classes that
|
|
114
|
+
# inherit from them (Types::QueryType, Types::NodeType, what `graphql:object`
|
|
115
|
+
# writes) lend their methods to the SDL's types of their name.
|
|
116
|
+
def adapt_type_bases
|
|
117
|
+
TYPE_CONCERN_ANCHORS.each do |name, anchor|
|
|
118
|
+
path = types_path("#{name}.rb")
|
|
119
|
+
next if !exist?(path) || read(path).include?('Lighthouse::GraphQL::Type')
|
|
120
|
+
|
|
121
|
+
indent = read(path)[anchor, 1]
|
|
122
|
+
next unless indent
|
|
123
|
+
|
|
124
|
+
indent += ' ' unless name == 'base_interface'
|
|
125
|
+
inject_into_file path, "#{indent}# Lighthouse's: the class of the SDL type of its name lends that type its methods.\n" \
|
|
126
|
+
"#{indent}include Lighthouse::GraphQL::Type\n", after: anchor
|
|
127
|
+
end
|
|
93
128
|
end
|
|
94
129
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
130
|
+
# graphql-ruby's controller, as its install wrote it, serves the schema with
|
|
131
|
+
# Lighthouse's concern. One the app has changed is left alone.
|
|
132
|
+
def adapt_controller
|
|
133
|
+
return if options[:skip_controller] || !exist?(CONTROLLER)
|
|
134
|
+
return if read(CONTROLLER).include?('Lighthouse::GraphQL::Controller')
|
|
135
|
+
return @kept_controller = true unless graphql_ruby_controller?(read(CONTROLLER))
|
|
98
136
|
|
|
99
137
|
template 'graphql_controller.rb.tt', CONTROLLER, force: true
|
|
100
138
|
end
|
|
@@ -106,8 +144,7 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
|
|
|
106
144
|
return unless File.exist?(routes)
|
|
107
145
|
return if File.read(routes).include?('graphql#execute')
|
|
108
146
|
|
|
109
|
-
# Byte-identical to graphql-ruby's install route
|
|
110
|
-
# runs second finds it in place and does not add it again.
|
|
147
|
+
# Byte-identical to graphql-ruby's install route.
|
|
111
148
|
route 'post "/graphql", to: "graphql#execute"'
|
|
112
149
|
end
|
|
113
150
|
|
|
@@ -128,73 +165,62 @@ class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
|
|
|
128
165
|
def show_next_steps
|
|
129
166
|
serve =
|
|
130
167
|
if @kept_controller
|
|
131
|
-
"Serve it: #{CONTROLLER} was
|
|
168
|
+
"Serve it: #{CONTROLLER} was changed from graphql-ruby's, so it was kept — " \
|
|
169
|
+
"include Lighthouse::GraphQL::Controller in it, or keep calling #{schema_name}.execute(...)."
|
|
132
170
|
else
|
|
133
171
|
'Query it: POST /graphql {"query": "{ users { data { id name } paginatorInfo { total } } }"}'
|
|
134
172
|
end
|
|
135
173
|
|
|
136
174
|
say <<~MSG
|
|
137
175
|
|
|
138
|
-
Lighthouse is installed. Next steps:
|
|
139
|
-
1. Describe your API in #{
|
|
140
|
-
2. Resolve a field in Ruby when a directive is not enough
|
|
141
|
-
(
|
|
142
|
-
|
|
176
|
+
Lighthouse is installed on graphql-ruby's install (#{schema_path}). Next steps:
|
|
177
|
+
1. Describe your API in #{sdl_schema_path} (the starter assumes a User model); #import more files, which `extend type Query`.
|
|
178
|
+
2. Resolve a field in Ruby when a directive is not enough, as in graphql-ruby: a method of
|
|
179
|
+
Types::QueryType (Query.latestPosts -> #latest_posts), a `field` of its own there,
|
|
180
|
+
`bin/rails g graphql:mutation`, or a Resolvers::BaseResolver subclass named after the field.
|
|
143
181
|
3. Give a type Ruby methods, a union or interface its resolve_type, a scalar its coercion:
|
|
144
|
-
bin/rails g graphql:object Post (or graphql:union, graphql:interface, graphql:scalar)
|
|
145
|
-
|
|
146
|
-
4.
|
|
182
|
+
bin/rails g graphql:object Post (or graphql:union, graphql:interface, graphql:scalar);
|
|
183
|
+
the class is Lighthouse's through its base (Lighthouse::GraphQL::Type)
|
|
184
|
+
4. The IDE helper files are rewritten in development; or: bin/rails lighthouse:ide_helper
|
|
147
185
|
5. #{serve}
|
|
148
186
|
MSG
|
|
149
187
|
end
|
|
150
188
|
|
|
151
189
|
private
|
|
152
190
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
say_status :skip, "#{path} already exists (pass --force to replace it)", :yellow
|
|
159
|
-
true
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def before?(path)
|
|
163
|
-
@before.include?(path)
|
|
191
|
+
def schema_superclass(indent)
|
|
192
|
+
lines = base_types.map { |key, name| "#{indent} #{key}: #{name}" }
|
|
193
|
+
"Lighthouse::GraphQL::RbLightHouse.get_schema(\n#{indent} options: {\n#{indent} base_types: {\n" \
|
|
194
|
+
"#{lines.join(",\n")}\n#{indent} }\n#{indent} }\n#{indent})"
|
|
164
195
|
end
|
|
165
196
|
|
|
166
|
-
def
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
# --skip-controller: the app serves the schema itself, so graphql-ruby's
|
|
175
|
-
# controller and route, if its install just wrote them, go.
|
|
176
|
-
def remove_graphql_ruby_controller
|
|
177
|
-
return if before?(CONTROLLER)
|
|
178
|
-
|
|
179
|
-
remove_file CONTROLLER
|
|
180
|
-
gsub_file 'config/routes.rb', %r{^\s*post "/graphql", to: "graphql#execute"\n}, '' if exist?('config/routes.rb')
|
|
197
|
+
def schema_comment(indent)
|
|
198
|
+
<<~RUBY.gsub(/^/, indent).delete_prefix(indent)
|
|
199
|
+
# Lighthouse builds the schema from the SDL in #{sdl_schema_path}, on the classes graphql-ruby's
|
|
200
|
+
# install wrote (base_types): the SDL's root types extend the code-first ones, whose
|
|
201
|
+
# `field`s stay the schema's, and every type, resolver and mutation class inherits from
|
|
202
|
+
# its kind's base. The rest of the class is graphql-ruby's.
|
|
203
|
+
RUBY
|
|
181
204
|
end
|
|
182
205
|
|
|
183
|
-
def
|
|
184
|
-
|
|
206
|
+
def add_dataloader
|
|
207
|
+
anchor = /^([ \t]*)use GraphQL::Batch\n/
|
|
208
|
+
indent = read(schema_path)[anchor, 1]
|
|
209
|
+
inject_into_file schema_path, "#{indent}# Lighthouse's directives batch through GraphQL::Dataloader, beside GraphQL::Batch.\n" \
|
|
210
|
+
"#{indent}use GraphQL::Dataloader\n", after: anchor
|
|
185
211
|
end
|
|
186
212
|
|
|
187
|
-
# `
|
|
188
|
-
def
|
|
189
|
-
|
|
213
|
+
# The SDL's entrypoint (Config.schema_path); `schema_path` is graphql-ruby's schema class.
|
|
214
|
+
def sdl_schema_path
|
|
215
|
+
options[:schema_path].delete_prefix('/')
|
|
190
216
|
end
|
|
191
217
|
|
|
192
218
|
def sdl_folder
|
|
193
|
-
|
|
219
|
+
File.dirname(sdl_schema_path)
|
|
194
220
|
end
|
|
195
221
|
|
|
196
|
-
def
|
|
197
|
-
|
|
222
|
+
def default_sdl_schema_path?
|
|
223
|
+
sdl_schema_path == 'graphql/schema.graphql'
|
|
198
224
|
end
|
|
199
225
|
|
|
200
226
|
def ide_helper_files
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# graphql-config, read by the GraphQL plugins for JetBrains IDEs and VS Code.
|
|
2
2
|
# The two generated files describe Lighthouse's directives and the types it
|
|
3
|
-
# generates
|
|
3
|
+
# generates. In development they are rewritten on boot and on every reload
|
|
4
|
+
# (config.ide_helper); elsewhere, refresh them with:
|
|
4
5
|
# bin/rails lighthouse:ide_helper
|
|
5
6
|
schema:
|
|
6
7
|
- <%= sdl_folder %>/**/*.graphql
|