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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +256 -1
  3. data/README.md +26 -15
  4. data/docs/caching.md +4 -3
  5. data/docs/configuration.md +16 -12
  6. data/docs/directives.md +294 -4
  7. data/docs/generators.md +84 -0
  8. data/docs/getting-started.md +93 -60
  9. data/docs/performance.md +2 -2
  10. data/docs/relationships.md +47 -4
  11. data/docs/relay.md +200 -0
  12. data/docs/resolvers.md +326 -34
  13. data/docs/security.md +73 -8
  14. data/docs/subscriptions.md +16 -9
  15. data/docs/types.md +218 -0
  16. data/docs/uploads.md +107 -0
  17. data/lib/generators/lighthouse/base_classes.rb +27 -49
  18. data/lib/generators/lighthouse/install/graphql_ruby_install.rb +113 -0
  19. data/lib/generators/lighthouse/install/install_generator.rb +125 -45
  20. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +10 -8
  21. data/lib/lighthouse/async_mutation_job.rb +17 -0
  22. data/lib/lighthouse/caching.rb +9 -3
  23. data/lib/lighthouse/complexity.rb +5 -4
  24. data/lib/lighthouse/config.rb +12 -18
  25. data/lib/lighthouse/controller.rb +53 -12
  26. data/lib/lighthouse/dataloader_sources/preload_loader.rb +16 -0
  27. data/lib/lighthouse/dataloader_sources/relation_counts_loader.rb +46 -0
  28. data/lib/lighthouse/default_resolve_dispatcher.rb +26 -0
  29. data/lib/lighthouse/directive_resolvers/all_resolver.rb +1 -1
  30. data/lib/lighthouse/directive_resolvers/argument_binding.rb +61 -0
  31. data/lib/lighthouse/directive_resolvers/argument_transforms_resolver.rb +220 -0
  32. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +2 -2
  33. data/lib/lighthouse/directive_resolvers/async_resolver.rb +90 -0
  34. data/lib/lighthouse/directive_resolvers/base.rb +63 -14
  35. data/lib/lighthouse/directive_resolvers/builder_applier.rb +89 -19
  36. data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +1 -1
  37. data/lib/lighthouse/directive_resolvers/can_resolvers.rb +1 -1
  38. data/lib/lighthouse/directive_resolvers/convention_resolver.rb +2 -1
  39. data/lib/lighthouse/directive_resolvers/count_resolver.rb +1 -1
  40. data/lib/lighthouse/directive_resolvers/field_resolver.rb +35 -7
  41. data/lib/lighthouse/directive_resolvers/first_resolver.rb +1 -1
  42. data/lib/lighthouse/directive_resolvers/global_id_resolver.rb +59 -0
  43. data/lib/lighthouse/directive_resolvers/lazy_load_resolver.rb +61 -0
  44. data/lib/lighthouse/directive_resolvers/limit_resolver.rb +59 -0
  45. data/lib/lighthouse/directive_resolvers/method_resolver.rb +38 -3
  46. data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +1 -1
  47. data/lib/lighthouse/directive_resolvers/namespaced_resolver.rb +31 -0
  48. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +1 -1
  49. data/lib/lighthouse/directive_resolvers/resolution.rb +19 -4
  50. data/lib/lighthouse/directive_resolvers/subscription_resolver.rb +2 -1
  51. data/lib/lighthouse/directive_resolvers/throttle_resolver.rb +136 -0
  52. data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +87 -0
  53. data/lib/lighthouse/directive_resolvers/with_count_resolver.rb +43 -0
  54. data/lib/lighthouse/directive_resolvers/with_resolver.rb +42 -0
  55. data/lib/lighthouse/directives/arguments/more_filters.rb +215 -0
  56. data/lib/lighthouse/directives/definitions.rb +52 -0
  57. data/lib/lighthouse/directives/model_directive.rb +131 -0
  58. data/lib/lighthouse/directives/node_directive.rb +134 -0
  59. data/lib/lighthouse/directives/pagination_manipulator.rb +1 -1
  60. data/lib/lighthouse/directives/registry.rb +21 -21
  61. data/lib/lighthouse/directives/visibility_directive.rb +71 -0
  62. data/lib/lighthouse/enum_directive.rb +41 -0
  63. data/lib/lighthouse/field_resolver_stack.rb +56 -6
  64. data/lib/lighthouse/generated_resolvers.rb +13 -8
  65. data/lib/lighthouse/global_id.rb +57 -0
  66. data/lib/lighthouse/graphql/version.rb +1 -1
  67. data/lib/lighthouse/graphql_ruby_patches.rb +45 -0
  68. data/lib/lighthouse/multipart_request.rb +43 -0
  69. data/lib/lighthouse/node_registry.rb +68 -0
  70. data/lib/lighthouse/pagination.rb +3 -0
  71. data/lib/lighthouse/rb_lighthouse.rb +34 -0
  72. data/lib/lighthouse/relation_counts.rb +29 -0
  73. data/lib/lighthouse/resolve_info.rb +80 -0
  74. data/lib/lighthouse/resolver_bases.rb +25 -4
  75. data/lib/lighthouse/resolver_dispatch.rb +51 -11
  76. data/lib/lighthouse/root_types.rb +117 -0
  77. data/lib/lighthouse/scalar_coercion.rb +72 -0
  78. data/lib/lighthouse/schema_cache.rb +2 -1
  79. data/lib/lighthouse/schema_generator.rb +13 -11
  80. data/lib/lighthouse/schema_implementation.rb +4 -8
  81. data/lib/lighthouse/support/environment.rb +13 -0
  82. data/lib/lighthouse/support/model_resolver.rb +7 -5
  83. data/lib/lighthouse/support/preload.rb +49 -0
  84. data/lib/lighthouse/type.rb +103 -0
  85. data/lib/lighthouse/type_resolution.rb +150 -0
  86. data/lib/lighthouse/upload.rb +28 -0
  87. data/lib/lighthouse/upload_store.rb +57 -0
  88. data/sig/lighthouse/directives.rbs +3 -2
  89. data/sig/lighthouse.rbs +10 -2
  90. metadata +40 -4
  91. data/lib/generators/lighthouse/install/templates/schema.rb.tt +0 -22
  92. data/lib/generators/lighthouse/mutation_generator.rb +0 -31
  93. 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
- # Resolvers::BaseResolver, Mutations::BaseMutation and
8
- # Subscriptions::BaseSubscription. The classes Lighthouse generates inherit from
9
- # them, so what every class of a kind shares (a concern, graphql-ruby's base type
10
- # hookups) is written once, in the base. graphql-ruby's `graphql:install` writes
11
- # the first two the same way.
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, and one written by graphql-ruby works as it is.
16
+ # the app's own code.
15
17
  module Lighthouse::Generators::BaseClasses
16
- BASES = {
17
- queries: {
18
- name: 'BaseResolver',
19
- source: <<~RUBY
20
- # What every class resolving a Query field shares: Lighthouse resolves
21
- # Query.latestPosts to Resolvers::LatestPosts by convention, and every such
22
- # class inherits from this one. graphql-ruby's `graphql:install` writes the
23
- # same class.
24
- class BaseResolver < GraphQL::Schema::Resolver
25
- end
26
- RUBY
27
- },
28
- mutations: {
29
- name: 'BaseMutation',
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
- BASES.dig(kind, :name)
38
+ NAMES[kind]
60
39
  end
61
40
 
62
- # Write the base class of `kind` into `namespace`, unless its file exists.
41
+ # Write the base class of `kind` into `namespace` when it is missing.
63
42
  def write_base_class(kind, namespace)
64
- base = BASES[kind]
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('::'), base[:source])
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