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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +212 -1
  3. data/README.md +38 -22
  4. data/docs/best-practices.md +6 -4
  5. data/docs/caching.md +4 -3
  6. data/docs/configuration.md +20 -26
  7. data/docs/custom-directives.md +2 -1
  8. data/docs/directives.md +79 -14
  9. data/docs/federation.md +1 -1
  10. data/docs/filtering-and-ordering.md +330 -30
  11. data/docs/generators.md +42 -40
  12. data/docs/getting-started.md +128 -58
  13. data/docs/motivation.md +3 -2
  14. data/docs/performance.md +5 -4
  15. data/docs/relationships.md +7 -5
  16. data/docs/relay.md +63 -0
  17. data/docs/resolvers.md +92 -39
  18. data/docs/schema-organisation.md +177 -0
  19. data/docs/security.md +64 -16
  20. data/docs/subscriptions.md +16 -9
  21. data/docs/types.md +16 -17
  22. data/docs/uploads.md +41 -14
  23. data/lib/generators/lighthouse/base_classes.rb +9 -14
  24. data/lib/generators/lighthouse/install/graphql_ruby_install.rb +113 -0
  25. data/lib/generators/lighthouse/install/install_generator.rb +125 -99
  26. data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +2 -1
  27. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +14 -13
  28. data/lib/lighthouse/ast_builder.rb +124 -0
  29. data/lib/lighthouse/caching.rb +9 -3
  30. data/lib/lighthouse/config.rb +27 -25
  31. data/lib/lighthouse/contracts/arg_manipulator.rb +9 -5
  32. data/lib/lighthouse/contracts/field_builder.rb +13 -0
  33. data/lib/lighthouse/controller.rb +28 -5
  34. data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +1 -2
  35. data/lib/lighthouse/directive_resolvers/all_resolver.rb +0 -2
  36. data/lib/lighthouse/directive_resolvers/argument_transforms_resolver.rb +18 -18
  37. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +9 -6
  38. data/lib/lighthouse/directive_resolvers/base.rb +6 -37
  39. data/lib/lighthouse/directive_resolvers/builder_applier.rb +21 -6
  40. data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +1 -1
  41. data/lib/lighthouse/directive_resolvers/count_resolver.rb +0 -1
  42. data/lib/lighthouse/directive_resolvers/first_resolver.rb +0 -2
  43. data/lib/lighthouse/directive_resolvers/global_id_resolver.rb +3 -1
  44. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +2 -3
  45. data/lib/lighthouse/directive_resolvers/throttle_resolver.rb +85 -26
  46. data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +12 -6
  47. data/lib/lighthouse/directives/arguments/more_filters.rb +1 -1
  48. data/lib/lighthouse/directives/arguments/order_by.rb +190 -26
  49. data/lib/lighthouse/directives/arguments/where_conditions.rb +25 -0
  50. data/lib/lighthouse/directives/arguments/where_conditions_base.rb +84 -0
  51. data/lib/lighthouse/directives/arguments/where_has_conditions.rb +56 -0
  52. data/lib/lighthouse/directives/definitions.rb +0 -1
  53. data/lib/lighthouse/directives/generated_types.rb +103 -0
  54. data/lib/lighthouse/directives/registry.rb +39 -10
  55. data/lib/lighthouse/document_ast.rb +111 -0
  56. data/lib/lighthouse/field_resolver_stack.rb +9 -6
  57. data/lib/lighthouse/global_id.rb +29 -3
  58. data/lib/lighthouse/graphql/version.rb +1 -1
  59. data/lib/lighthouse/graphql_ruby_patches.rb +45 -0
  60. data/lib/lighthouse/ide_helper.rb +80 -23
  61. data/lib/lighthouse/namespaces.rb +1 -0
  62. data/lib/lighthouse/order_by/types.rb +137 -0
  63. data/lib/lighthouse/railtie.rb +9 -2
  64. data/lib/lighthouse/rb_lighthouse.rb +16 -5
  65. data/lib/lighthouse/resolve_info.rb +5 -5
  66. data/lib/lighthouse/resolver_bases.rb +18 -4
  67. data/lib/lighthouse/resolver_dispatch.rb +16 -1
  68. data/lib/lighthouse/root_types.rb +117 -0
  69. data/lib/lighthouse/schema_cache.rb +8 -4
  70. data/lib/lighthouse/schema_factory.rb +6 -9
  71. data/lib/lighthouse/schema_generator.rb +8 -18
  72. data/lib/lighthouse/schema_stitcher.rb +64 -0
  73. data/lib/lighthouse/support/naming.rb +0 -12
  74. data/lib/lighthouse/upload.rb +4 -2
  75. data/lib/lighthouse/upload_store.rb +39 -14
  76. data/lib/lighthouse/where_conditions/columns.rb +36 -0
  77. data/lib/lighthouse/where_conditions/handler.rb +152 -0
  78. data/lib/lighthouse/where_conditions/json_column.rb +99 -0
  79. data/lib/lighthouse/where_conditions/operator_map.rb +1 -4
  80. data/lib/lighthouse/where_conditions/types.rb +110 -0
  81. data/sig/lighthouse/directives.rbs +16 -3
  82. data/sig/lighthouse/graphql.rbs +48 -4
  83. data/sig/lighthouse.rbs +8 -12
  84. metadata +17 -7
  85. data/lib/generators/lighthouse/install/templates/schema.rb.tt +0 -29
  86. data/lib/generators/lighthouse/mutation_generator.rb +0 -31
  87. data/lib/generators/lighthouse/query_generator.rb +0 -30
  88. data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +0 -170
  89. data/lib/lighthouse/directives/where_conditions_directive.rb +0 -244
  90. data/lib/lighthouse/sdl_loader.rb +0 -22
@@ -37,12 +37,14 @@ Override it with `key:` — above, `minAge` filters the `age` column.
37
37
 
38
38
  ### `@where` operators
39
39
 
40
- `@where(operator: "...")` supports the same operator vocabulary as
41
- `@whereConditions`:
40
+ `@where(operator: "...")` takes these operators:
42
41
 
43
42
  `EQ`, `NEQ`, `GT`, `GTE`, `LT`, `LTE`, `IN`, `NIN`, `CONTAINS`, `STARTS_WITH`,
44
43
  `ENDS_WITH`, `IS_NULL`, `IS_NOT_NULL`.
45
44
 
45
+ They are not `@whereConditions`' `SQLOperator` values, which follow PHP
46
+ Lighthouse (see below).
47
+
46
48
  ```graphql
47
49
  type Query {
48
50
  events(
@@ -52,38 +54,192 @@ type Query {
52
54
  }
53
55
  ```
54
56
 
55
- ## Ordering — `@orderBy`
57
+ ## Ordering
58
+
59
+ `@orderBy` works as in PHP Lighthouse. On an argument, the client picks the
60
+ order. On a field, the order is fixed.
61
+
62
+ ### Client-controlled ordering
63
+
64
+ Write the argument's type as `_`. Lighthouse then generates an input that
65
+ takes the allowed columns as an enum, together with the `SortOrder` enum:
66
+
67
+ ```graphql
68
+ type Query {
69
+ posts(orderBy: _ @orderBy(columns: ["posted_at", "title"])): [Post!]! @all
70
+ }
71
+ ```
72
+
73
+ Lighthouse generates this from it:
74
+
75
+ ```graphql
76
+ "Order by clause for Query.posts.orderBy."
77
+ input QueryPostsOrderByOrderByClause {
78
+ "The column that is used for ordering."
79
+ column: QueryPostsOrderByColumn!
80
+
81
+ "The direction that is used for ordering."
82
+ order: SortOrder!
83
+ }
84
+
85
+ "Allowed column names for Query.posts.orderBy."
86
+ enum QueryPostsOrderByColumn {
87
+ POSTED_AT @enum(value: "posted_at")
88
+ TITLE @enum(value: "title")
89
+ }
90
+
91
+ "Directions for ordering a list of records."
92
+ enum SortOrder {
93
+ ASC
94
+ DESC
95
+ }
96
+ ```
97
+
98
+ The argument becomes `orderBy: [QueryPostsOrderByOrderByClause!]`. The client
99
+ passes a list of clauses, which apply in turn:
100
+
101
+ ```graphql
102
+ {
103
+ posts(orderBy: [{ column: POSTED_AT, order: DESC }, { column: TITLE, order: ASC }]) {
104
+ title
105
+ }
106
+ }
107
+ ```
108
+
109
+ The names follow `<Type><Field><Argument>`: `QueryPostsOrderBy…` for
110
+ `Query.posts(orderBy:)`. Each clause needs an `order`. Without `columns:`,
111
+ `columnsEnum:` or `relations:`, the argument takes the shared `OrderByClause`,
112
+ whose `column` is a `String!`. That allows any column, so restrict the columns
113
+ whenever clients are not trusted.
56
114
 
57
- Sort the result by one or more columns. The client passes a list of clauses, each
58
- with a `column` and an `order` (`ASC`/`DESC`); clauses apply in order.
115
+ To reuse one enum on several fields, name it with `columnsEnum:`. Its
116
+ `@enum(value:)` gives each value's column:
59
117
 
60
118
  ```graphql
61
119
  type Query {
62
- users(orderBy: [OrderByClause!] @orderBy): [User!]! @all
120
+ allPosts(orderBy: _ @orderBy(columnsEnum: "PostColumn")): [Post!]! @all
121
+ paginatedPosts(orderBy: _ @orderBy(columnsEnum: "PostColumn")): [Post!]! @paginate
122
+ }
63
123
 
64
- # query:
65
- # users(orderBy: [{ column: NAME, order: ASC }, { column: CREATED_AT, order: DESC }])
124
+ enum PostColumn {
125
+ POSTED_AT @enum(value: "posted_at")
126
+ TITLE @enum(value: "title")
66
127
  }
67
128
  ```
68
129
 
69
- `@orderBy` also accepts `{ field:, direction: }` clause shapes, so it slots into
70
- existing client conventions.
130
+ `columns:` and `columnsEnum:` cannot be used together.
131
+
132
+ ### Ordering by a relation
133
+
134
+ `relations:` lets the client order by an aggregate of a relation. Without
135
+ columns, that aggregate is `COUNT`. With a relation's own `columns:` (or
136
+ `columnsEnum:`), it is `AVG`, `MIN`, `MAX`, `SUM` or `COUNT` of one of them:
137
+
138
+ ```graphql
139
+ type Query {
140
+ users(
141
+ orderBy: _ @orderBy(columns: ["name"], relations: [{ relation: "posts" }, { relation: "tasks", columns: ["difficulty"] }])
142
+ ): [User!]! @all
143
+ }
144
+ ```
145
+
146
+ ```graphql
147
+ {
148
+ users(
149
+ orderBy: [
150
+ { posts: { aggregate: COUNT }, order: DESC }
151
+ { tasks: { aggregate: MAX, column: DIFFICULTY }, order: DESC }
152
+ { column: NAME, order: ASC }
153
+ ]
154
+ ) {
155
+ name
156
+ }
157
+ }
158
+ ```
159
+
160
+ The argument then takes `[QueryUsersOrderByRelationOrderByClause!]`. Each clause
161
+ names exactly one of `column` and the relations. Each relation has an input of
162
+ its own, `QueryUsersOrderByPosts` / `QueryUsersOrderByTasks`, with the
163
+ `OrderByRelationAggregateFunction` or
164
+ `OrderByRelationWithColumnAggregateFunction` enum.
165
+
166
+ The aggregate is computed once per record, in a `LEFT JOIN` on a grouped
167
+ subquery. A record with no related records is still listed, with a `COUNT` of 0.
168
+ This also works on a relationship field (`posts(orderBy: _ @orderBy(...)): [Post!]! @hasMany`),
169
+ including when its pages are batched.
170
+
171
+ ### Predefined ordering
172
+
173
+ On a field, `@orderBy(column:, direction:)` always orders the result, whatever
174
+ the client sends:
175
+
176
+ ```graphql
177
+ type Query {
178
+ latestUsers: [User!]! @all @orderBy(column: "created_at", direction: DESC)
179
+ }
180
+ ```
181
+
182
+ A field's `@orderBy` applies after the client's `orderBy` argument, so on a
183
+ field with both, it breaks ties:
184
+
185
+ ```graphql
186
+ type Query {
187
+ posts(orderBy: _ @orderBy(columns: ["title"])): [Post!]! @paginate @orderBy(column: "created_at", direction: DESC)
188
+ }
189
+ ```
190
+
191
+ An argument written with a type of its own, such as `orderBy: [MyClause!] @orderBy`,
192
+ keeps that type. Its input needs a `column` and an `order`.
71
193
 
72
194
  ## Complex where conditions
73
195
 
74
- For a flexible, client-controlled `WHERE` builder, use `@whereConditions`.
75
- Lighthouse generates the input and column-enum types automatically — you only
76
- declare the allowed columns.
196
+ `@whereConditions` works as in PHP Lighthouse. The client builds the `WHERE`
197
+ clause itself, from conditions joined by `AND` and `OR`, and from `HAS` checks on
198
+ relations. Write the argument's type as `_`, and declare the allowed columns:
77
199
 
78
200
  ```graphql
79
201
  type Query {
80
202
  people(
81
- where: _ @whereConditions(columns: ["age", "type", "height"])
203
+ where: _ @whereConditions(columns: ["age", "type", "haircolour", "height"])
82
204
  ): [Person!]! @all
83
205
  }
84
206
  ```
85
207
 
86
- Clients can then build dynamic conditions with `AND` / `OR` / operators:
208
+ Lighthouse generates these types for `Query.people(where:)`:
209
+
210
+ ```graphql
211
+ "Dynamic WHERE conditions for the `where` argument of Query.people."
212
+ input QueryPeopleWhereWhereConditions {
213
+ column: QueryPeopleWhereColumn
214
+ operator: SQLOperator = EQ
215
+ value: Mixed
216
+ AND: [QueryPeopleWhereWhereConditions!]
217
+ OR: [QueryPeopleWhereWhereConditions!]
218
+ HAS: QueryPeopleWhereWhereConditionsRelation
219
+ }
220
+
221
+ "Allowed column names for Query.people.where."
222
+ enum QueryPeopleWhereColumn {
223
+ AGE @enum(value: "age")
224
+ TYPE @enum(value: "type")
225
+ HAIRCOLOUR @enum(value: "haircolour")
226
+ HEIGHT @enum(value: "height")
227
+ }
228
+
229
+ input QueryPeopleWhereWhereConditionsRelation {
230
+ relation: String!
231
+ operator: SQLOperator = GTE
232
+ amount: Int = 1
233
+ condition: QueryPeopleWhereWhereConditionsHasCondition
234
+ }
235
+ ```
236
+
237
+ `QueryPeopleWhereWhereConditionsHasCondition` has the same fields as the
238
+ conditions input, but its `column` is a `String`. It filters the related
239
+ records, whose columns are not the model's.
240
+
241
+ The conditions of one level all hold. An `AND` list is a group whose conditions
242
+ all hold, and an `OR` list is a group where at least one does:
87
243
 
88
244
  ```graphql
89
245
  {
@@ -91,38 +247,182 @@ Clients can then build dynamic conditions with `AND` / `OR` / operators:
91
247
  where: {
92
248
  AND: [
93
249
  { column: AGE, operator: GT, value: 37 }
94
- { OR: [
95
- { column: TYPE, value: "Actor" }
250
+ { column: TYPE, value: "Actor" }
251
+ {
252
+ OR: [
253
+ { column: HAIRCOLOUR, value: "red" }
96
254
  { column: HEIGHT, operator: GTE, value: 150 }
97
- ] }
255
+ ]
256
+ }
257
+ ]
258
+ }
259
+ ) {
260
+ name
261
+ }
262
+ }
263
+ ```
264
+
265
+ ```graphql
266
+ {
267
+ people(
268
+ where: {
269
+ AND: [
270
+ { column: HAIRCOLOUR, operator: IS_NULL }
271
+ { column: HEIGHT, operator: BETWEEN, value: [150, 200] }
98
272
  ]
99
273
  }
100
- ) { name }
274
+ ) {
275
+ name
276
+ }
101
277
  }
102
278
  ```
103
279
 
104
- The generated `SQLOperator` enum covers `EQ`, `NEQ`, `GT`, `GTE`, `LT`, `LTE`,
105
- `IN`, `NIN`, `CONTAINS`, `STARTS_WITH`, `ENDS_WITH`, `IS_NULL`, `IS_NOT_NULL`.
280
+ `where: null` filters nothing.
281
+
282
+ ### Operators
106
283
 
107
- ### `@whereHasConditions`
284
+ The generated `SQLOperator` enum has PHP Lighthouse's operators. `IS_NULL` and
285
+ `IS_NOT_NULL` take no value. Any other operator needs one, or the query fails
286
+ with "Did not receive a value to match the WhereConditions for column …".
108
287
 
109
- Filter by the existence of a related record, with conditions applied to the
110
- relation:
288
+ | Operator | SQL | `value` |
289
+ | --- | --- | --- |
290
+ | `EQ` (default) | `=` | a scalar |
291
+ | `NEQ` | `!=` | a scalar |
292
+ | `GT`, `GTE`, `LT`, `LTE` | `>`, `>=`, `<`, `<=` | a scalar |
293
+ | `LIKE`, `NOT_LIKE` | `LIKE`, `NOT LIKE` | a pattern: `"%Rails%"` |
294
+ | `IN`, `NOT_IN` | `IN`, `NOT IN` | a list |
295
+ | `BETWEEN`, `NOT_BETWEEN` | `BETWEEN`, `NOT BETWEEN` | `[from, to]` |
296
+ | `IS_NULL`, `IS_NOT_NULL` | `IS NULL`, `IS NOT NULL` | none |
297
+
298
+ ### Related records — `HAS`
299
+
300
+ `HAS` keeps the records that have related records. By default, at least one,
301
+ which is `operator: GTE, amount: 1`. Its `condition` filters the related
302
+ records, on their own table's columns:
303
+
304
+ ```graphql
305
+ {
306
+ users(where: { HAS: { relation: "posts", amount: 3, condition: { column: "published", value: true } } }) {
307
+ name
308
+ }
309
+ }
310
+ ```
311
+
312
+ `relation` names an association of the model (`posts`, or `blogPosts` for
313
+ `blog_posts`). `{ relation: "posts", operator: LT, amount: 1 }` keeps the users
314
+ with no posts. A `condition` can hold its own `HAS`, which checks the related
315
+ records' relations.
316
+
317
+ ### Without `columns:`
318
+
319
+ Without `columns:` or `columnsEnum:`, the argument takes the shared
320
+ `WhereConditions` input, whose `column` is a `String`. A column a client
321
+ names there must be an identifier or `table.column`, optionally followed by a
322
+ [JSON path](#json-columns) (`meta->tags`). Anything else fails with "Column names
323
+ may contain only alphanumerics or underscores, optionally a table and a JSON path
324
+ (posts.meta->tags), and may not begin with a digit, got: …". Any column of the
325
+ table can still be filtered on, so declare the allowed columns when clients are
326
+ not trusted.
327
+
328
+ ### JSON columns
329
+
330
+ As in PHP Lighthouse (and Laravel), a column can point into a JSON document with
331
+ `->`: `meta->series`, `meta->author->name`, and `meta->tags->0` for an array
332
+ element. `@whereConditions`, `@whereHasConditions` and `@orderBy`, on an argument
333
+ or on a field, take it in `columns:` like any column:
111
334
 
112
335
  ```graphql
113
336
  type Query {
114
- users(
115
- hasPosts: _ @whereHasConditions(columns: ["status"])
116
- ): [User!]! @all
337
+ posts(
338
+ where: _ @whereConditions(columns: ["title", "meta->series", "meta->rank"])
339
+ orderBy: _ @orderBy(columns: ["meta->rank"])
340
+ ): [Post!]! @all
341
+ rankedPosts: [Post!]! @all @orderBy(column: "meta->rank", direction: DESC)
342
+ }
343
+ ```
344
+
345
+ The generated enum names the path with `__` (`META__SERIES`, `META__AUTHOR__NAME`):
346
+
347
+ ```graphql
348
+ {
349
+ posts(
350
+ where: { AND: [{ column: META__SERIES, value: "graphql" }, { column: META__RANK, operator: GT, value: 9 }] }
351
+ orderBy: [{ column: META__RANK, order: DESC }]
352
+ ) {
353
+ title
354
+ }
355
+ }
356
+ ```
357
+
358
+ The value at the path is compiled for the database: `"meta"->>'series'` on
359
+ PostgreSQL, `JSON_UNQUOTE(JSON_EXTRACT(meta, '$."series"'))` on MySQL, and
360
+ `json_extract(meta, '$."series"')` on SQLite. Unlike Laravel, it behaves the same
361
+ on all three:
362
+
363
+ - A number compares as a number (`meta->rank GT 9` keeps 10), and a boolean as a
364
+ boolean. `LIKE` compares the text.
365
+ - `IS_NULL` holds for a missing key and for a JSON `null`.
366
+ - `@orderBy` orders by the JSON value, so numbers sort numerically.
367
+
368
+ ### A custom handler
369
+
370
+ `handler:` names a class, looked up in `Config.namespaces[:builders]` (`Builders::`).
371
+ It applies the conditions instead of Lighthouse:
372
+
373
+ ```graphql
374
+ type Query {
375
+ people(where: _ @whereConditions(columns: ["age"], handler: "PeopleConditions")): [Person!]! @all
117
376
  }
118
377
  ```
119
378
 
120
- The relation is inferred from the `has<Relation>` argument name (`hasPosts` →
121
- `posts`), or specified with `relation:`.
379
+ ```ruby
380
+ class Builders::PeopleConditions
381
+ # `conditions` as the client sent them: { column: "age", operator: "GT", value: 37, and: [...], ... }
382
+ def self.call(relation, conditions)
383
+ relation.where(...)
384
+ end
385
+ end
386
+ ```
387
+
388
+ Use `"Class@method"` to call another method.
389
+
390
+ ## `@whereHasConditions`
391
+
392
+ `@whereHasConditions` keeps the records that have at least one related record
393
+ matching the client's conditions. The input is the same as `@whereConditions`',
394
+ applied to the relation:
395
+
396
+ ```graphql
397
+ type Query {
398
+ people(hasRole: _ @whereHasConditions(columns: ["name", "access_level"])): [Person!]! @all
399
+ }
400
+ ```
401
+
402
+ ```graphql
403
+ {
404
+ people(hasRole: { column: ACCESS_LEVEL, operator: GTE, value: 5 }) {
405
+ name
406
+ }
407
+ }
408
+ ```
409
+
410
+ The relation is the argument's name without `has` (`hasRole` → `role`, and
411
+ `roles` if the model has that instead). You can also name it with `relation:`:
412
+ `work: _ @whereHasConditions(relation: "tasks")`. The generated input is
413
+ `QueryPeopleHasRoleWhereHasConditions`.
414
+
415
+ `hasRole: {}` keeps the people with any role. `hasRole: null` filters nothing.
416
+
417
+ The client's conditions are an argument directive like any other, so they also
418
+ work on `@paginate`, `@count`, `@first`, the relationship directives, and
419
+ `resolve_info.enhance_builder`. The argument can have any name.
122
420
 
123
421
  ## How columns are resolved
124
422
 
125
423
  Column names from enums (e.g. `CREATED_AT`) and arguments (`minAge`) are mapped to
126
424
  real columns through one central rule (`Lighthouse::Support::Naming`): ALLCAPS
127
425
  enum keys are downcased, and camelCase is converted to snake_case. This keeps
128
- client-facing GraphQL idiomatic while matching your Rails schema.
426
+ client-facing GraphQL idiomatic while matching your Rails schema. An enum
427
+ value's `@enum(value:)` names its column directly, as in the enums `@orderBy`
428
+ and `@whereConditions` generate.
data/docs/generators.md CHANGED
@@ -2,51 +2,51 @@
2
2
 
3
3
  Lighthouse builds on graphql-ruby, and so do its generators: it writes only what
4
4
  graphql-ruby has no generator for, or what an SDL-first schema needs differently.
5
- Every graphql-ruby app's base classes (`Types::BaseObject`, `Resolvers::BaseResolver`,
6
- `Mutations::BaseMutation`, ...) come from graphql-ruby's own `graphql:install`.
5
+ Every graphql-ruby app's schema class and base classes (`Types::BaseObject`,
6
+ `Resolvers::BaseResolver`, `Mutations::BaseMutation`, ...) come from graphql-ruby's
7
+ own `graphql:install`, and `lighthouse:install` builds on them.
7
8
 
8
9
  ## `lighthouse:install`
9
10
 
10
11
  ```shell
12
+ bin/rails generate graphql:install # graphql-ruby's, with the options you choose
11
13
  bin/rails generate lighthouse:install
12
14
  ```
13
15
 
14
- It runs graphql-ruby's install first
15
- (`graphql:install --no-relay --skip-graphiql --skip-query-logs`), which writes:
16
+ It works on what graphql-ruby's `graphql:install` wrote, with whichever of its
17
+ options the app chose (`--relay`, the default, `--batch`, `--skip-mutation-root-type`,
18
+ `--schema`, `--directory`, `--api`, ...). When the app has no graphql-ruby schema
19
+ class yet, it runs `graphql:install` first, with graphql-ruby's defaults (and
20
+ `--api` when `config.api_only = true`). Then it adapts what graphql-ruby wrote:
16
21
 
17
- - `app/graphql/types/base_*.rb`: `BaseObject`, `BaseField`, `BaseArgument`,
18
- `BaseInterface`, `BaseUnion`, `BaseScalar`, `BaseEnum`, `BaseInputObject`;
19
- - `app/graphql/resolvers/base_resolver.rb` and `app/graphql/mutations/base_mutation.rb`;
20
- - its schema class, `Types::QueryType`, `Types::MutationType` and controller, and the
21
- `POST /graphql` route.
22
+ | File | What Lighthouse does |
23
+ | --- | --- |
24
+ | `app/graphql/app_schema.rb` | `class AppSchema < GraphQL::Schema` becomes `class AppSchema < Lighthouse::GraphQL::RbLightHouse.get_schema(options: { base_types: { ... } })`, naming every class graphql-ruby wrote. The rest of the class stays, but the `query(...)` / `mutation(...)` lines (the SDL's root types are built on those classes) and the `resolve_type` stub (Lighthouse resolves types). With `--batch`, `use GraphQL::Dataloader` goes beside `use GraphQL::Batch`, as Lighthouse's directives batch through the Dataloader |
25
+ | `app/graphql/types/query_type.rb`, `mutation_type.rb` | The example `test_field` goes. Relay's `node` / `nodes` stay: the SDL's `Query` is built on `Types::QueryType`, so its `field`s are the schema's too |
26
+ | `app/graphql/types/base_object.rb`, `base_interface.rb`, `base_union.rb`, `base_scalar.rb` | `include Lighthouse::GraphQL::Type`: every type class inheriting from them is Lighthouse's, `Types::QueryType` and `Types::NodeType` included |
27
+ | `app/graphql/mutations/base_mutation.rb` | Kept as graphql-ruby wrote it, a `RelayClassicMutation` (see [resolvers.md](resolvers.md#resolution-by-convention)) |
28
+ | `app/controllers/graphql_controller.rb` | Replaced by one including `Lighthouse::GraphQL::Controller` (`lighthouse_schema AppSchema`), when it is still as graphql-ruby wrote it. One the app changed is kept, and the install says how to serve the schema |
22
29
 
23
- Then Lighthouse puts its own on top of what graphql-ruby wrote in that run:
30
+ And it adds Lighthouse's own:
24
31
 
25
- | File | What Lighthouse does |
32
+ | File | What it is |
26
33
  | --- | --- |
27
- | `app/graphql/app_schema.rb` | Replaced by the schema built from the SDL, naming every base class in `base_types` |
28
- | `app/controllers/graphql_controller.rb` | Replaced by one including `Lighthouse::GraphQL::Controller` |
29
- | `app/graphql/types/query_type.rb`, `mutation_type.rb` | Removed: the SDL declares the root types |
30
- | `app/graphql/mutations/base_mutation.rb` | Made a `GraphQL::Schema::Mutation`: the SDL declares a mutation's arguments, where graphql-ruby's `RelayClassicMutation` takes a single `input:` |
31
- | `app/graphql/subscriptions/base_subscription.rb` | Written: graphql-ruby has no subscription base. It includes `Lighthouse::GraphQL::Subscription` |
32
- | `config/initializers/lighthouse.rb` | Written: every [configuration](configuration.md) option, commented out |
34
+ | `config/initializers/lighthouse.rb` | Every [configuration](configuration.md) option, commented out |
33
35
  | `graphql/schema.graphql` | A starter schema, unless the folder holds `.graphql` files already |
34
36
  | `graphql.config.yml` | IDE configuration, and `.gitignore` entries for the [IDE helper](getting-started.md#ide-support) files |
37
+ | `app/graphql/subscriptions/base_subscription.rb` | With `--subscriptions` only: graphql-ruby has no subscription base. It includes `Lighthouse::GraphQL::Subscription` |
38
+ | `config/routes.rb` | `post "/graphql", to: "graphql#execute"`, when missing |
35
39
 
36
40
  | Option | Effect |
37
41
  | --- | --- |
38
- | `--schema=ApiSchema` | Name of the schema constant (default `AppSchema`) |
39
- | `--sdl-folder=app/schema` | Where the `.graphql` files live (default `graphql`); sets `config.sdl_folder` |
40
- | `--skip-controller` | No controller or route (graphql-ruby's are removed too) — for apps that serve GraphQL themselves |
42
+ | `--schema=ApiSchema` | Which graphql-ruby schema class to adopt, and its name when `graphql:install` has to run |
43
+ | `--schema-path=app/schema/schema.graphql` | The schema's entrypoint, which imports the other `.graphql` files (default `graphql/schema.graphql`); sets `config.schema_path` |
44
+ | `--subscriptions` | Write `Subscriptions::BaseSubscription` and name it in `base_types` |
45
+ | `--skip-controller` | Leave graphql-ruby's controller as it is, and add no route — for apps that serve GraphQL themselves |
41
46
  | `--skip-ide` | No `graphql.config.yml` or `.gitignore` entries |
42
- | `--force` | Replace a schema class or controller the app had before |
43
47
 
44
- Rerunning it is safe: a file the app has is kept, and the route and `.gitignore`
45
- entries are added once. In an app that already had a graphql-ruby schema class or
46
- controller, those are kept (Lighthouse replaces only what graphql-ruby wrote in the
47
- same run), and graphql-ruby's install points that schema at the root types it
48
- wrote: have the controller call `AppSchema.execute(...)` to serve Lighthouse's
49
- schema, or pass `--force`.
48
+ Rerunning it changes nothing: a schema class that is Lighthouse's already, a file
49
+ the app has, the route and the `.gitignore` entries are left as they are.
50
50
 
51
51
  ## Lighthouse's generators
52
52
 
@@ -56,27 +56,29 @@ schema. Each writes into the first namespace configured for its kind (or
56
56
 
57
57
  | Generator | Writes |
58
58
  | --- | --- |
59
- | `lighthouse:query LatestPosts` | `Resolvers::LatestPosts < BaseResolver`, which `Query.latestPosts` resolves to by convention |
60
- | `lighthouse:mutation PublishPost` | `Mutations::PublishPost < BaseMutation`, for `Mutation.publishPost` |
61
59
  | `lighthouse:subscription PostUpdated` | `Subscriptions::PostUpdated < BaseSubscription`, with its hooks (and the base, when missing) |
62
60
  | `lighthouse:builder Published` | `Builders::Published`, for `@builder(class: "Published")` |
63
61
  | `lighthouse:validator CreatePostInput` | `Validators::CreatePostInputValidator`, a `Dry::Validation::Contract` |
64
62
 
65
- The class's arguments and return type are the SDL's, so it declares neither. When
66
- the base class is missing (an app that never ran an install, or a namespace of its
67
- own such as `Api::Resolvers`), the generator says so: `lighthouse:install` writes it
68
- through graphql-ruby, or write `Api::Resolvers::BaseResolver` yourself.
63
+ A subscription's arguments and return type are the SDL's, so the class declares
64
+ neither. When `Subscriptions::BaseSubscription` is missing, `lighthouse:subscription`
65
+ writes it.
66
+
67
+ A resolver or mutation class has no Lighthouse generator: write a
68
+ `Resolvers::BaseResolver` subclass by hand (see [resolvers.md](resolvers.md)), add a
69
+ method to `Types::QueryType`, or run graphql-ruby's `graphql:mutation`.
69
70
 
70
71
  ## graphql-ruby's generators in a Lighthouse app
71
72
 
72
73
  | Generator | In a Lighthouse app |
73
74
  | --- | --- |
74
- | `graphql:install` | Run by `lighthouse:install`. On its own, it writes a code-first schema, root types and a `RelayClassicMutation` base: run `lighthouse:install` instead |
75
- | `graphql:object Post` | **Use it** for a [type class](resolvers.md#type-classes): add `include Lighthouse::GraphQL::Type`, then methods. The `field` lines it writes (from the model's columns, when there is one) are ignored, as the SDL declares the fields: delete them |
76
- | `graphql:scalar DateTime` | **Use it** for a [scalar](types.md#scalars): add `include Lighthouse::GraphQL::Type`; its `coerce_input` / `coerce_result` are the ones Lighthouse calls |
77
- | `graphql:union SearchResult`, `graphql:interface Authored` | **Use them** when the [default resolution](types.md#the-default) is not enough: add `include Lighthouse::GraphQL::Type` and a `resolve_type`. Its `possible_types` and `field` lines are ignored |
75
+ | `graphql:install` | The base `lighthouse:install` builds on: run it first with the options you want, or let `lighthouse:install` run it with its defaults |
76
+ | `graphql:object Post` | **Use it** for a [type class](resolvers.md#type-classes): it inherits from `Types::BaseObject`, so it is Lighthouse's; add methods. The `field` lines it writes (from the model's columns, when there is one) are ignored, as the SDL declares the fields: delete them |
77
+ | `graphql:scalar DateTime` | **Use it** for a [scalar](types.md#scalars): its `coerce_input` / `coerce_result` are the ones Lighthouse calls |
78
+ | `graphql:union SearchResult`, `graphql:interface Authored` | **Use them** when the [default resolution](types.md#the-default) is not enough: add a `resolve_type`. Their `possible_types` and `field` lines are ignored |
78
79
  | `graphql:enum`, `graphql:input` | Not needed: the SDL declares enums and inputs ([`@enum`](directives.md#enum) gives a value an internal one) |
79
- | `graphql:mutation`, `graphql:mutation_create` / `_update` / `_delete` | **Not ready**: they add a field to `Types::MutationType`, which a Lighthouse app has none of (they write it again), and write a `RelayClassicMutation` taking `input:`. Use `lighthouse:mutation`, or [`@create`, `@update`, `@delete`](mutations.md) |
80
- | `graphql:relay` | Not needed: [`@node`, `@globalId`](relay.md) and `@paginate(type: CONNECTION)` |
80
+ | `graphql:mutation PublishPost` | **Use it** for a code-first mutation: it writes `Mutations::PublishPost` and adds `field :publish_post, mutation: Mutations::PublishPost` to `Types::MutationType`, which the SDL's `Mutation` is built on. The class declares its own arguments and fields, as in graphql-ruby |
81
+ | `graphql:mutation_create` / `_update` / `_delete` | Code-first mutations like `graphql:mutation`'s, whose payload and input are code-first types (`Types::PostType`, `Types::PostInputType`). For a type the SDL declares, use [`@create`, `@update`, `@delete`](mutations.md) |
82
+ | `graphql:relay` | Run by `graphql:install` (`--relay` is its default). On an app installed with `--no-relay`, it adds `Types::NodeType`, `node` / `nodes` and the schema's `id_from_object` / `object_from_id`: add `node: Types::NodeType` to `base_types` by hand. See [relay.md](relay.md#relay-through-graphql-ruby) |
81
83
  | `graphql:loader` | A GraphQL::Batch loader, which your own resolver classes may use; Lighthouse's directives batch through graphql-ruby's Dataloader instead |
82
- | `graphql:detailed_trace` | **Not ready**: it edits a `class AppSchema < GraphQL::Schema`, which Lighthouse's `AppSchema = get_schema(...)` is not. Add its migration and `AppSchema.use(GraphQL::Tracing::DetailedTrace)` by hand |
84
+ | `graphql:detailed_trace` | **Not ready**: it looks for `class AppSchema < GraphQL::Schema`, which a Lighthouse schema class (`< Lighthouse::GraphQL::RbLightHouse.get_schema(...)`) does not match, and stops. Add `use GraphQL::Tracing::DetailedTrace` and `def self.detailed_trace?(query)` to the class body by hand, with graphql-ruby's migration (or `redis:`) |