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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +95 -1
- data/README.md +15 -7
- data/docs/best-practices.md +6 -4
- data/docs/configuration.md +19 -5
- data/docs/custom-directives.md +2 -1
- data/docs/directives.md +65 -7
- data/docs/federation.md +1 -1
- data/docs/filtering-and-ordering.md +330 -30
- data/docs/generators.md +1 -1
- data/docs/getting-started.md +43 -10
- data/docs/motivation.md +3 -2
- data/docs/performance.md +4 -3
- data/docs/relationships.md +2 -1
- data/docs/resolvers.md +2 -2
- data/docs/schema-organisation.md +177 -0
- data/lib/generators/lighthouse/install/install_generator.rb +16 -11
- data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +2 -1
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +11 -4
- data/lib/lighthouse/ast_builder.rb +124 -0
- data/lib/lighthouse/config.rb +23 -4
- data/lib/lighthouse/contracts/arg_manipulator.rb +9 -5
- data/lib/lighthouse/contracts/field_builder.rb +13 -0
- 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/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/count_resolver.rb +0 -1
- data/lib/lighthouse/directive_resolvers/first_resolver.rb +0 -2
- data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +2 -3
- 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/graphql/version.rb +1 -1
- 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 +15 -5
- data/lib/lighthouse/resolve_info.rb +5 -5
- data/lib/lighthouse/schema_cache.rb +8 -4
- data/lib/lighthouse/schema_factory.rb +6 -9
- data/lib/lighthouse/schema_generator.rb +4 -15
- data/lib/lighthouse/schema_stitcher.rb +64 -0
- data/lib/lighthouse/support/naming.rb +0 -12
- 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 -2
- metadata +15 -4
- 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
|
@@ -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: "...")`
|
|
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
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
120
|
+
allPosts(orderBy: _ @orderBy(columnsEnum: "PostColumn")): [Post!]! @all
|
|
121
|
+
paginatedPosts(orderBy: _ @orderBy(columnsEnum: "PostColumn")): [Post!]! @paginate
|
|
122
|
+
}
|
|
63
123
|
|
|
64
|
-
|
|
65
|
-
|
|
124
|
+
enum PostColumn {
|
|
125
|
+
POSTED_AT @enum(value: "posted_at")
|
|
126
|
+
TITLE @enum(value: "title")
|
|
66
127
|
}
|
|
67
128
|
```
|
|
68
129
|
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
95
|
-
|
|
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
|
-
) {
|
|
274
|
+
) {
|
|
275
|
+
name
|
|
276
|
+
}
|
|
101
277
|
}
|
|
102
278
|
```
|
|
103
279
|
|
|
104
|
-
|
|
105
|
-
|
|
280
|
+
`where: null` filters nothing.
|
|
281
|
+
|
|
282
|
+
### Operators
|
|
106
283
|
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
-
| `--
|
|
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 |
|
data/docs/getting-started.md
CHANGED
|
@@ -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` |
|
|
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`, `--
|
|
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
|
-
|
|
52
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
102
|
-
|
|
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
|
|
63
|
-
`config.watchable_dirs`, and an edit
|
|
64
|
-
the
|
|
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
|
data/docs/relationships.md
CHANGED
|
@@ -174,7 +174,8 @@ queries:
|
|
|
174
174
|
type User {
|
|
175
175
|
publishedPosts(
|
|
176
176
|
title: String @like
|
|
177
|
-
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:
|
|
289
|
+
myPosts(title: String @like, orderBy: _ @orderBy(columns: ["title", "created_at"])): [Post!]! @guard
|
|
290
290
|
}
|
|
291
291
|
```
|
|
292
292
|
|