lighthouse-graphql 0.6.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +95 -1
  3. data/README.md +15 -7
  4. data/docs/best-practices.md +6 -4
  5. data/docs/configuration.md +19 -5
  6. data/docs/custom-directives.md +2 -1
  7. data/docs/directives.md +65 -7
  8. data/docs/federation.md +1 -1
  9. data/docs/filtering-and-ordering.md +330 -30
  10. data/docs/generators.md +1 -1
  11. data/docs/getting-started.md +43 -10
  12. data/docs/motivation.md +3 -2
  13. data/docs/performance.md +4 -3
  14. data/docs/relationships.md +2 -1
  15. data/docs/resolvers.md +2 -2
  16. data/docs/schema-organisation.md +177 -0
  17. data/lib/generators/lighthouse/install/install_generator.rb +16 -11
  18. data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +2 -1
  19. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +11 -4
  20. data/lib/lighthouse/ast_builder.rb +124 -0
  21. data/lib/lighthouse/config.rb +23 -4
  22. data/lib/lighthouse/contracts/arg_manipulator.rb +9 -5
  23. data/lib/lighthouse/contracts/field_builder.rb +13 -0
  24. data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +1 -2
  25. data/lib/lighthouse/directive_resolvers/all_resolver.rb +0 -2
  26. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +9 -6
  27. data/lib/lighthouse/directive_resolvers/base.rb +6 -37
  28. data/lib/lighthouse/directive_resolvers/builder_applier.rb +21 -6
  29. data/lib/lighthouse/directive_resolvers/count_resolver.rb +0 -1
  30. data/lib/lighthouse/directive_resolvers/first_resolver.rb +0 -2
  31. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +2 -3
  32. data/lib/lighthouse/directives/arguments/order_by.rb +190 -26
  33. data/lib/lighthouse/directives/arguments/where_conditions.rb +25 -0
  34. data/lib/lighthouse/directives/arguments/where_conditions_base.rb +84 -0
  35. data/lib/lighthouse/directives/arguments/where_has_conditions.rb +56 -0
  36. data/lib/lighthouse/directives/definitions.rb +0 -1
  37. data/lib/lighthouse/directives/generated_types.rb +103 -0
  38. data/lib/lighthouse/directives/registry.rb +39 -10
  39. data/lib/lighthouse/document_ast.rb +111 -0
  40. data/lib/lighthouse/graphql/version.rb +1 -1
  41. data/lib/lighthouse/ide_helper.rb +80 -23
  42. data/lib/lighthouse/namespaces.rb +1 -0
  43. data/lib/lighthouse/order_by/types.rb +137 -0
  44. data/lib/lighthouse/railtie.rb +9 -2
  45. data/lib/lighthouse/rb_lighthouse.rb +15 -5
  46. data/lib/lighthouse/resolve_info.rb +5 -5
  47. data/lib/lighthouse/schema_cache.rb +8 -4
  48. data/lib/lighthouse/schema_factory.rb +6 -9
  49. data/lib/lighthouse/schema_generator.rb +4 -15
  50. data/lib/lighthouse/schema_stitcher.rb +64 -0
  51. data/lib/lighthouse/support/naming.rb +0 -12
  52. data/lib/lighthouse/where_conditions/columns.rb +36 -0
  53. data/lib/lighthouse/where_conditions/handler.rb +152 -0
  54. data/lib/lighthouse/where_conditions/json_column.rb +99 -0
  55. data/lib/lighthouse/where_conditions/operator_map.rb +1 -4
  56. data/lib/lighthouse/where_conditions/types.rb +110 -0
  57. data/sig/lighthouse/directives.rbs +16 -3
  58. data/sig/lighthouse/graphql.rbs +48 -4
  59. data/sig/lighthouse.rbs +8 -2
  60. metadata +15 -4
  61. data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +0 -170
  62. data/lib/lighthouse/directives/where_conditions_directive.rb +0 -244
  63. 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
@@ -40,7 +40,7 @@ And it adds Lighthouse's own:
40
40
  | Option | Effect |
41
41
  | --- | --- |
42
42
  | `--schema=ApiSchema` | Which graphql-ruby schema class to adopt, and its name when `graphql:install` has to run |
43
- | `--sdl-folder=app/schema` | Where the `.graphql` files live (default `graphql`); sets `config.sdl_folder` |
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
44
  | `--subscriptions` | Write `Subscriptions::BaseSubscription` and name it in `base_types` |
45
45
  | `--skip-controller` | Leave graphql-ruby's controller as it is, and add no route — for apps that serve GraphQL themselves |
46
46
  | `--skip-ide` | No `graphql.config.yml` or `.gitignore` entries |
@@ -37,10 +37,10 @@ graphql-ruby wrote, and adds Lighthouse's own:
37
37
  | `app/graphql/types/base_*.rb`, `resolvers/base_resolver.rb`, `mutations/base_mutation.rb` | graphql-ruby's base classes. `BaseObject`, `BaseInterface`, `BaseUnion` and `BaseScalar` include `Lighthouse::GraphQL::Type` |
38
38
  | `app/controllers/graphql_controller.rb` | graphql-ruby's controller, replaced by one including `Lighthouse::GraphQL::Controller` |
39
39
  | `config/initializers/lighthouse.rb` | Every [configuration](configuration.md) option, commented out at its default |
40
- | `graphql/schema.graphql` | A starter schema (`user` via `@find`, `users` via `@paginate`) that assumes a `User` model — skipped when the folder already holds `.graphql` files |
40
+ | `graphql/schema.graphql` | The schema's entrypoint: a starter schema (`user` via `@find`, `users` via `@paginate`) that assumes a `User` model. Skipped when the file exists |
41
41
  | `graphql.config.yml` | IDE configuration — see [IDE support](#ide-support) |
42
42
 
43
- `--schema=ApiSchema`, `--sdl-folder=app/schema`, `--subscriptions`,
43
+ `--schema=ApiSchema`, `--schema-path=app/schema/schema.graphql`, `--subscriptions`,
44
44
  `--skip-controller` and `--skip-ide` change what it does. Rerunning it changes
45
45
  nothing, and a controller the app changed is kept.
46
46
  [generators.md](generators.md) has the details, every generator Lighthouse has, and
@@ -48,8 +48,8 @@ which of graphql-ruby's `graphql:*` generators fit a Lighthouse app.
48
48
 
49
49
  ## 1. Write your schema (SDL)
50
50
 
51
- By default Lighthouse loads every `*.graphql` file under `RAILS_ROOT/graphql`,
52
- sorted by path and concatenated. Split your schema however you like:
51
+ Lighthouse reads the schema from one entrypoint, `RAILS_ROOT/graphql/schema.graphql`
52
+ (`config.schema_path`), as PHP Lighthouse does:
53
53
 
54
54
  ```graphql
55
55
  # graphql/schema.graphql
@@ -70,6 +70,32 @@ argument (the `@eq` makes `id` a filter). Lighthouse infers the model from the
70
70
  field's return type name (`User`), or you can be explicit with
71
71
  `@all(model: "User")`.
72
72
 
73
+ To split the schema, the entrypoint imports the other files. A type is defined
74
+ once, and the other files add to it with `extend type`:
75
+
76
+ ```graphql
77
+ # graphql/schema.graphql
78
+ type Query
79
+
80
+ #import user.graphql
81
+ #import blog/*.graphql
82
+ ```
83
+
84
+ ```graphql
85
+ # graphql/user.graphql
86
+ type User {
87
+ id: ID!
88
+ name: String
89
+ }
90
+
91
+ extend type Query {
92
+ users: [User!]! @all
93
+ }
94
+ ```
95
+
96
+ A file nothing imports is not part of the schema, and a type defined twice is an
97
+ error. See [schema-organisation.md](schema-organisation.md).
98
+
73
99
  ## 2. Provide your base classes
74
100
 
75
101
  Lighthouse builds on graphql-ruby, and follows its convention: each kind of class
@@ -158,8 +184,8 @@ class AppSchema < Lighthouse::GraphQL::RbLightHouse.get_schema(
158
184
  end
159
185
  ```
160
186
 
161
- The SDL is read from `config.sdl_folder` (`get_schema(sdl_folder: ...)` overrides
162
- it). The class body is graphql-ruby's, so plugins and settings go there as in any
187
+ The SDL is read from `config.schema_path` and the files it imports
188
+ (`get_schema(schema_path: ...)` overrides it). The class body is graphql-ruby's, so plugins and settings go there as in any
163
189
  graphql-ruby schema (`use ...`, `max_complexity(...)`), but for `query(...)` and
164
190
  `mutation(...)`, which the SDL's root types replace, and `resolve_type`, which
165
191
  Lighthouse answers (see [types.md](types.md)).
@@ -260,7 +286,9 @@ Your IDE validates and autocompletes `.graphql` files against the schema it can
260
286
  see. Lighthouse's directive definitions and the types its manipulation phase
261
287
  generates live in Ruby, so without help the IDE flags `@paginate` as unknown and
262
288
  can't resolve `UserPaginator`. PHP Lighthouse solves this with an IDE helper
263
- command; so does Lighthouse for Rails:
289
+ command, and so does Lighthouse for Rails. In development, an app with a
290
+ `graphql.config.yml` gets the files rewritten on boot and on every reload
291
+ (`config.ide_helper`), so you never run it by hand there. Elsewhere:
264
292
 
265
293
  ```shell
266
294
  bin/rails lighthouse:ide_helper # writes to Rails.root
@@ -274,8 +302,11 @@ It writes two files:
274
302
  declares (so the IDE never sees a duplicate).
275
303
  - **`programmatic-types.graphql`** — the types Lighthouse adds to the executable
276
304
  schema: `<Type>Paginator`, `<Type>SimplePaginator`, `<Type>Connection`/`Edge`,
277
- `PaginatorInfo`, `SimplePaginatorInfo`, `PageInfo`, the `whereConditions`
278
- inputs and enums, and the `_` placeholder scalar.
305
+ `PaginatorInfo`, `SimplePaginatorInfo`, `PageInfo`, the `@whereConditions` and
306
+ `@orderBy` inputs and enums, and the `_` placeholder scalar (without it, the
307
+ IDE reports `Unknown type "_"`). It leaves out every type the SDL or
308
+ `schema-directives.graphql` defines, because graphql-js, which the IDE plugins
309
+ use, rejects a type defined twice.
279
310
 
280
311
  Point your IDE's [graphql-config](https://the-guild.dev/graphql/config) at them
281
312
  next to your SDL — the install generator writes this `graphql.config.yml`:
@@ -289,7 +320,9 @@ schema:
289
320
 
290
321
  It works with the GraphQL plugin for JetBrains IDEs and the GraphQL extension for
291
322
  VS Code. The files are derived from your schema, so the generator adds them to
292
- `.gitignore`; rerun the task after changing the schema or registering a directive.
323
+ `.gitignore`. Outside development, rerun the task after changing the schema or
324
+ registering a directive. The IDE reads every `.graphql` file the glob matches, as
325
+ one schema: it does not follow `#import`.
293
326
 
294
327
  > The IDE reads fields as you wrote them: `users: [User!]! @paginate` still looks
295
328
  > like a list to it, although clients query `users { data { … } }`. Field
data/docs/motivation.md CHANGED
@@ -98,8 +98,9 @@ generated from Ruby classes:
98
98
  - **It is designed before it is implemented.** You can agree on the types and fields
99
99
  first, as a team, and the server follows the design rather than leaking its internals.
100
100
  - **Tools understand it directly.** IDE plugins, code generators and schema registries
101
- read SDL. Lighthouse's `lighthouse:ide_helper` task adds its directives and generated
102
- types so editors validate and autocomplete the schema.
101
+ read SDL. Lighthouse writes out its directives and generated types (in development
102
+ on every reload, or with `lighthouse:ide_helper`), so editors validate and
103
+ autocomplete the schema.
103
104
  - **It says what each field does.** `posts: [Post!]! @paginate @guard @cacheControl(maxAge: 60)`
104
105
  tells a reader, without opening any Ruby, that the field is paginated, requires a
105
106
  signed-in user and can be cached for a minute.
data/docs/performance.md CHANGED
@@ -59,9 +59,10 @@ Lighthouse.configure { |config| config.watch_sdl = Rails.env.development? }
59
59
  ```
60
60
 
61
61
  Rails' reloader watches Ruby files, not `.graphql` ones, so without this an SDL
62
- edit only shows up after restarting the server. With it, the SDL folder joins
63
- `config.watchable_dirs`, and an edit reloads the app, and with it the schema, on
64
- the next request, like a Ruby change. This relies on the schema class living
62
+ edit only shows up after restarting the server. With it, the folder of
63
+ `config.schema_path` joins `config.watchable_dirs`, and an edit to any `.graphql`
64
+ file below it reloads the app, and with it the schema, on the next request, like
65
+ a Ruby change. The reload also rewrites the IDE helper files (`config.ide_helper`). This relies on the schema class living
65
66
  in an autoloaded file, as `app/graphql/app_schema.rb` does.
66
67
 
67
68
  ## Persisted queries
@@ -174,7 +174,8 @@ queries:
174
174
  type User {
175
175
  publishedPosts(
176
176
  title: String @like
177
- orderBy: [OrderByClause!] @orderBy
177
+ orderBy: _ @orderBy(columns: ["title", "created_at"])
178
+ where: _ @whereConditions(columns: ["title"])
178
179
  ): [Post!]! @hasMany(relation: "posts", scopes: ["published"])
179
180
  }
180
181
  ```
data/docs/resolvers.md CHANGED
@@ -282,11 +282,11 @@ What PHP Lighthouse hands a resolver as `$resolveInfo`. A `Resolvers::` or
282
282
  `enhance_builder` lets a class build the query no directive could, and still honour
283
283
  what the field declares: its `@builder` classes, and the argument directives the
284
284
  client used (`@eq`, `@where`, `@in`, `@like`, `@orderBy`, `@whereConditions`, ...),
285
- as `@all` and `@paginate` apply them. `scopes:` adds model scopes.
285
+ then the field's own `@orderBy(column:)`, as `@all` and `@paginate` apply them. `scopes:` adds model scopes.
286
286
 
287
287
  ```graphql
288
288
  type Query {
289
- myPosts(title: String @like, orderBy: [PostOrderByClause!] @orderBy): [Post!]! @guard
289
+ myPosts(title: String @like, orderBy: _ @orderBy(columns: ["title", "created_at"])): [Post!]! @guard
290
290
  }
291
291
  ```
292
292