@vantreeseba/drizzle-graphql 5.6.0 → 7.0.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.
- package/README.md +1020 -768
- package/dist/README.md +1020 -768
- package/dist/index.cjs +1653 -548
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +238 -2
- package/dist/index.d.ts +238 -2
- package/dist/index.js +1578 -457
- package/dist/index.js.map +1 -1
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/README.md
CHANGED
|
@@ -1,768 +1,1020 @@
|
|
|
1
|
-
# Drizzle-GraphQL
|
|
2
|
-
|
|
3
|
-
Automatically create GraphQL schema or customizable schema config fields from Drizzle ORM schema
|
|
4
|
-
|
|
5
|
-
## Usage
|
|
6
|
-
|
|
7
|
-
- Pass your drizzle database instance and schema into builder to generate `{ schema, entities }` object
|
|
8
|
-
- Use `schema` if pre-built schema already satisfies all your neeeds. It's compatible witn any server that consumes `GraphQLSchema` class instance
|
|
9
|
-
|
|
10
|
-
Example: hosting schema using [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server)
|
|
11
|
-
|
|
12
|
-
```Typescript
|
|
13
|
-
import { createServer } from 'node:http'
|
|
14
|
-
import { createYoga } from 'graphql-yoga'
|
|
15
|
-
import { buildSchema } from 'drizzle-graphql'
|
|
16
|
-
|
|
17
|
-
// db - your drizzle instance
|
|
18
|
-
import { db } from './database'
|
|
19
|
-
|
|
20
|
-
const { schema } = buildSchema(db)
|
|
21
|
-
|
|
22
|
-
const yoga = createYoga({ schema })
|
|
23
|
-
|
|
24
|
-
server.listen(4000, () => {
|
|
25
|
-
console.info('Server is running on http://localhost:4000/graphql')
|
|
26
|
-
})
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
- If you want to customize your schema, you can use `entities` object to build your own new schema
|
|
30
|
-
|
|
31
|
-
```Typescript
|
|
32
|
-
import { createServer } from 'node:http'
|
|
33
|
-
import { GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLSchema } from 'graphql'
|
|
34
|
-
import { createYoga } from 'graphql-yoga'
|
|
35
|
-
import { buildSchema } from 'drizzle-graphql'
|
|
36
|
-
|
|
37
|
-
// Schema contains 'Users' and 'Customers' tables
|
|
38
|
-
import { db } from './database'
|
|
39
|
-
|
|
40
|
-
const { entities } = buildSchema(db)
|
|
41
|
-
|
|
42
|
-
// You can customize which parts of queries or mutations you want
|
|
43
|
-
const schema = new GraphQLSchema({
|
|
44
|
-
query: new GraphQLObjectType({
|
|
45
|
-
name: 'Query',
|
|
46
|
-
fields: {
|
|
47
|
-
// Select only wanted queries out of all generated
|
|
48
|
-
users: entities.queries.users,
|
|
49
|
-
customer: entities.queries.customersSingle,
|
|
50
|
-
|
|
51
|
-
// Create a custom one
|
|
52
|
-
customUsers: {
|
|
53
|
-
// You can reuse and customize types from original schema
|
|
54
|
-
type: new GraphQLList(new GraphQLNonNull(entities.types.UsersItem)),
|
|
55
|
-
args: {
|
|
56
|
-
// You can reuse inputs as well
|
|
57
|
-
where: {
|
|
58
|
-
type: entities.inputs.UsersFilters
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
resolve: async (source, args, context, info) => {
|
|
62
|
-
// Your custom logic goes here...
|
|
63
|
-
const result = await db.select(schema.Users).where()...
|
|
64
|
-
|
|
65
|
-
return result
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}),
|
|
70
|
-
// Same rules apply to mutations
|
|
71
|
-
mutation: new GraphQLObjectType({
|
|
72
|
-
name: 'Mutation',
|
|
73
|
-
fields: entities.mutations
|
|
74
|
-
}),
|
|
75
|
-
// In case you need types inside your schema
|
|
76
|
-
types: [...Object.values(entities.types), ...Object.values(entities.inputs)]
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
const yoga = createYoga({
|
|
80
|
-
schema
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
server.listen(4000, () => {
|
|
84
|
-
console.info('Server is running on http://localhost:4000/graphql')
|
|
85
|
-
})
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
## Choosing what gets generated
|
|
89
|
-
|
|
90
|
-
Every generated operation is on by default. `features` turns individual ones off, which
|
|
91
|
-
keeps them out of the schema, out of `entities`, and out of the type map — a schema for a
|
|
92
|
-
read-only API, or one that never exposes aggregates, does not pay for types nobody can
|
|
93
|
-
reach:
|
|
94
|
-
|
|
95
|
-
```Typescript
|
|
96
|
-
const { schema } = buildSchema(db, {
|
|
97
|
-
features: {
|
|
98
|
-
aggregates: false, // <plural>Aggregate root queries
|
|
99
|
-
groupBy: false, // <plural>GroupBy root queries
|
|
100
|
-
relationAggregates: false, // <relation>Aggregate fields on object types
|
|
101
|
-
distinct: false, // the `distinct` argument on list queries
|
|
102
|
-
insert: false, // create<Table> / create<Table>Single mutations
|
|
103
|
-
update: false, // update<Table> / update<Table>Single mutations
|
|
104
|
-
delete: false, // delete<Table> / delete<Table>Single mutations
|
|
105
|
-
upsert: true, // upsert<Table> / upsert<Table>Single mutations (off by default)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
|
132
|
-
|
|
|
133
|
-
| `
|
|
134
|
-
| `
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
- **`
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
- **`
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
A
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
`
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
- `
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
{
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
{
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
- `
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
{
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
- The
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
- `
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
```
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
`
|
|
758
|
-
|
|
759
|
-
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
1
|
+
# Drizzle-GraphQL
|
|
2
|
+
|
|
3
|
+
Automatically create GraphQL schema or customizable schema config fields from Drizzle ORM schema
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
- Pass your drizzle database instance and schema into builder to generate `{ schema, entities }` object
|
|
8
|
+
- Use `schema` if pre-built schema already satisfies all your neeeds. It's compatible witn any server that consumes `GraphQLSchema` class instance
|
|
9
|
+
|
|
10
|
+
Example: hosting schema using [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server)
|
|
11
|
+
|
|
12
|
+
```Typescript
|
|
13
|
+
import { createServer } from 'node:http'
|
|
14
|
+
import { createYoga } from 'graphql-yoga'
|
|
15
|
+
import { buildSchema } from 'drizzle-graphql'
|
|
16
|
+
|
|
17
|
+
// db - your drizzle instance
|
|
18
|
+
import { db } from './database'
|
|
19
|
+
|
|
20
|
+
const { schema } = buildSchema(db)
|
|
21
|
+
|
|
22
|
+
const yoga = createYoga({ schema })
|
|
23
|
+
|
|
24
|
+
server.listen(4000, () => {
|
|
25
|
+
console.info('Server is running on http://localhost:4000/graphql')
|
|
26
|
+
})
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- If you want to customize your schema, you can use `entities` object to build your own new schema
|
|
30
|
+
|
|
31
|
+
```Typescript
|
|
32
|
+
import { createServer } from 'node:http'
|
|
33
|
+
import { GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLSchema } from 'graphql'
|
|
34
|
+
import { createYoga } from 'graphql-yoga'
|
|
35
|
+
import { buildSchema } from 'drizzle-graphql'
|
|
36
|
+
|
|
37
|
+
// Schema contains 'Users' and 'Customers' tables
|
|
38
|
+
import { db } from './database'
|
|
39
|
+
|
|
40
|
+
const { entities } = buildSchema(db)
|
|
41
|
+
|
|
42
|
+
// You can customize which parts of queries or mutations you want
|
|
43
|
+
const schema = new GraphQLSchema({
|
|
44
|
+
query: new GraphQLObjectType({
|
|
45
|
+
name: 'Query',
|
|
46
|
+
fields: {
|
|
47
|
+
// Select only wanted queries out of all generated
|
|
48
|
+
users: entities.queries.users,
|
|
49
|
+
customer: entities.queries.customersSingle,
|
|
50
|
+
|
|
51
|
+
// Create a custom one
|
|
52
|
+
customUsers: {
|
|
53
|
+
// You can reuse and customize types from original schema
|
|
54
|
+
type: new GraphQLList(new GraphQLNonNull(entities.types.UsersItem)),
|
|
55
|
+
args: {
|
|
56
|
+
// You can reuse inputs as well
|
|
57
|
+
where: {
|
|
58
|
+
type: entities.inputs.UsersFilters
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
resolve: async (source, args, context, info) => {
|
|
62
|
+
// Your custom logic goes here...
|
|
63
|
+
const result = await db.select(schema.Users).where()...
|
|
64
|
+
|
|
65
|
+
return result
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}),
|
|
70
|
+
// Same rules apply to mutations
|
|
71
|
+
mutation: new GraphQLObjectType({
|
|
72
|
+
name: 'Mutation',
|
|
73
|
+
fields: entities.mutations
|
|
74
|
+
}),
|
|
75
|
+
// In case you need types inside your schema
|
|
76
|
+
types: [...Object.values(entities.types), ...Object.values(entities.inputs)]
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const yoga = createYoga({
|
|
80
|
+
schema
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
server.listen(4000, () => {
|
|
84
|
+
console.info('Server is running on http://localhost:4000/graphql')
|
|
85
|
+
})
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Choosing what gets generated
|
|
89
|
+
|
|
90
|
+
Every generated operation is on by default. `features` turns individual ones off, which
|
|
91
|
+
keeps them out of the schema, out of `entities`, and out of the type map — a schema for a
|
|
92
|
+
read-only API, or one that never exposes aggregates, does not pay for types nobody can
|
|
93
|
+
reach:
|
|
94
|
+
|
|
95
|
+
```Typescript
|
|
96
|
+
const { schema } = buildSchema(db, {
|
|
97
|
+
features: {
|
|
98
|
+
aggregates: false, // <plural>Aggregate root queries
|
|
99
|
+
groupBy: false, // <plural>GroupBy root queries
|
|
100
|
+
relationAggregates: false, // <relation>Aggregate fields on object types
|
|
101
|
+
distinct: false, // the `distinct` argument on list queries
|
|
102
|
+
insert: false, // create<Table> / create<Table>Single mutations
|
|
103
|
+
update: false, // update<Table> / update<Table>Single mutations
|
|
104
|
+
delete: false, // delete<Table> / delete<Table>Single mutations
|
|
105
|
+
upsert: true, // upsert<Table> / upsert<Table>Single mutations (off by default)
|
|
106
|
+
nestedWrites: true, // relation fields on the create/update inputs (off by default)
|
|
107
|
+
fieldUpdateOperations: true, // increment/push operations on the update input (off by default)
|
|
108
|
+
countMutations: true, // update<Table>Count / delete<Table>Count mutations (off by default)
|
|
109
|
+
requireWhere: true, // make `where` non-null on plural update/delete (off by default)
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
- Any flag left out keeps its default of `true`, so `{ features: { delete: false } }`
|
|
115
|
+
changes nothing else
|
|
116
|
+
- `upsert`, `nestedWrites`, `fieldUpdateOperations` and `countMutations` are the
|
|
117
|
+
exceptions: all four default to `false`, so they only exist if you ask for them
|
|
118
|
+
- `groupBy` needs `aggregates`: it reuses those output types, so turning `aggregates` off
|
|
119
|
+
turns the group-by queries off with it
|
|
120
|
+
- Turning off `insert` or `update` also drops the input type that only that mutation
|
|
121
|
+
used (`Create<Type>Input` / `Update<Type>Input`)
|
|
122
|
+
- Turning off all three mutation features omits the `Mutation` type entirely, the same as
|
|
123
|
+
`mutations: false`
|
|
124
|
+
- List and single queries are always generated, so `Query` is never empty
|
|
125
|
+
|
|
126
|
+
## Scalars
|
|
127
|
+
|
|
128
|
+
Columns whose values don't fit a built-in GraphQL scalar get a named custom scalar, so the
|
|
129
|
+
generated SDL says what a field actually holds instead of falling back to `String`:
|
|
130
|
+
|
|
131
|
+
| Drizzle column | GraphQL type | Transported as |
|
|
132
|
+
| ------------------------------------------ | ------------ | ------------------------------------ |
|
|
133
|
+
| `json` / `jsonb` (and `mode: 'json'`) | `JSON` | the parsed value |
|
|
134
|
+
| `bigint` (and `mode: 'bigint'`) | `BigInt` | a decimal string |
|
|
135
|
+
| `uuid` | `UUID` | a validated UUID string |
|
|
136
|
+
| `timestamp` / `datetime` | `DateTime` | an ISO-8601 string |
|
|
137
|
+
| `date` | `Date` | a `YYYY-MM-DD` string |
|
|
138
|
+
|
|
139
|
+
```graphql
|
|
140
|
+
mutation {
|
|
141
|
+
createDocumentSingle(values: { id: "11111111-1111-4111-8111-111111111111", payload: { tags: ["a"], views: 3 }, counter: "9007199254740993" }) {
|
|
142
|
+
payload
|
|
143
|
+
counter
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- **`JSON`** carries the value itself — objects, arrays, numbers, strings, `true`/`false`.
|
|
149
|
+
Reads return the parsed value, not a stringified one, and writes take a literal or a
|
|
150
|
+
variable rather than a string of JSON
|
|
151
|
+
- **`BigInt`** is always a decimal string in both directions, so values past
|
|
152
|
+
`Number.MAX_SAFE_INTEGER` survive the round-trip. Integer literals are accepted on input;
|
|
153
|
+
floats and non-numeric strings are rejected
|
|
154
|
+
- **`UUID`** validates the format on the way in, including inside `where` filters
|
|
155
|
+
|
|
156
|
+
The scalars are exported if you need them in a hand-written schema:
|
|
157
|
+
|
|
158
|
+
```Typescript
|
|
159
|
+
import { GraphQLBigIntString, GraphQLDate, GraphQLDateTime, GraphQLJSON, GraphQLUUID } from 'drizzle-graphql'
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`GraphQLBigIntString` is the `BigInt` scalar — named for what it does rather than what it is
|
|
163
|
+
called in SDL, to avoid clashing with the language's own `BigInt`.
|
|
164
|
+
|
|
165
|
+
## Enum types
|
|
166
|
+
|
|
167
|
+
A column declared with a database enum becomes a GraphQL enum named after **the enum**, not
|
|
168
|
+
after the column that happens to use it:
|
|
169
|
+
|
|
170
|
+
```Typescript
|
|
171
|
+
export const status = pgEnum('status', ['active', 'archived'])
|
|
172
|
+
|
|
173
|
+
export const Users = pgTable('users', { status: status('status') })
|
|
174
|
+
export const Posts = pgTable('posts', { status: status('status') })
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
```graphql
|
|
178
|
+
enum StatusEnum {
|
|
179
|
+
active
|
|
180
|
+
archived
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
type Users {
|
|
184
|
+
status: StatusEnum!
|
|
185
|
+
}
|
|
186
|
+
type Posts {
|
|
187
|
+
status: StatusEnum!
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
- One type, one filter input (`StatusEnumFilter`), so a client variable typed `StatusEnum!`
|
|
192
|
+
is passable to either table
|
|
193
|
+
- The declared name is PascalCased and suffixed: `user_status` → `UserStatusEnum`
|
|
194
|
+
- A column that lists its values inline (`text('tier', { enum: ['free', 'paid'] })`) has no
|
|
195
|
+
declared name to share, so it keeps a per-column type — `UsersTierEnum`
|
|
196
|
+
- Two different enums that would land on the same GraphQL name (`status` in two Postgres
|
|
197
|
+
schemas) fail the build rather than silently sharing a type
|
|
198
|
+
|
|
199
|
+
`enumNameMapper` overrides the name. Returning `undefined` keeps the default, so a mapper
|
|
200
|
+
only has to handle the cases it cares about:
|
|
201
|
+
|
|
202
|
+
```Typescript
|
|
203
|
+
const { schema } = buildSchema(db, {
|
|
204
|
+
// `schema` is the Postgres schema the enum was declared in, if any; `values` is its
|
|
205
|
+
// value list. Everything but `enumName` is always present.
|
|
206
|
+
enumNameMapper: ({ enumName, schema }) =>
|
|
207
|
+
schema === 'reporting' ? `Reporting${enumName}Enum` : undefined,
|
|
208
|
+
})
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Two columns the mapper sends to the same name share one type, as long as their values match —
|
|
212
|
+
which is also how inline enums can be unified. Sending the same name to two different value
|
|
213
|
+
lists is the build-time error above.
|
|
214
|
+
|
|
215
|
+
> **Upgrading:** before this, every enum column got its own `<Table><Column>Enum`. A column
|
|
216
|
+
> backed by a declared enum now reports the shared name instead (`UsersStatusEnum` →
|
|
217
|
+
> `StatusEnum`). To keep the old names, return them from the mapper:
|
|
218
|
+
>
|
|
219
|
+
> ```Typescript
|
|
220
|
+
> enumNameMapper: ({ tableName, columnName }) =>
|
|
221
|
+
> `${tableName}${columnName[0]!.toUpperCase()}${columnName.slice(1)}Enum`
|
|
222
|
+
> ```
|
|
223
|
+
|
|
224
|
+
## Documenting the schema
|
|
225
|
+
|
|
226
|
+
Four hooks put descriptions and deprecations on the generated types. Each is called per
|
|
227
|
+
column (or per table / per relation) and returns `undefined` to say nothing:
|
|
228
|
+
|
|
229
|
+
```Typescript
|
|
230
|
+
const { schema } = buildSchema(db, {
|
|
231
|
+
describeColumn: (column, { tableName, columnName }) => docs[tableName]?.[columnName],
|
|
232
|
+
describeTable: (tableName) => docs[tableName]?._table,
|
|
233
|
+
describeRelation: (tableName, relationName) => `The ${relationName} of this ${tableName}`,
|
|
234
|
+
deprecateColumn: (column, { columnName }) => (columnName === 'legacyId' ? 'Use `id`' : undefined),
|
|
235
|
+
})
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
- `describeColumn` reaches every place the column appears: the object type, the create and
|
|
239
|
+
update inputs, the filter input's field, `Min`/`Max` aggregates and `GroupKeys`
|
|
240
|
+
- The hook receives the drizzle column itself, so a project that keeps its documentation in
|
|
241
|
+
the schema (a `.$type()` brand, a custom column property, a comment table) can read it
|
|
242
|
+
straight off
|
|
243
|
+
- `deprecateColumn` marks output fields and *optional* input fields. A required input field
|
|
244
|
+
is skipped: GraphQL forbids deprecating one, and emitting it would make the schema invalid
|
|
245
|
+
- Filter and `orderBy` fields are never deprecated — filtering on a deprecated column is how
|
|
246
|
+
a caller finds the rows still using it
|
|
247
|
+
|
|
248
|
+
## Relation filters
|
|
249
|
+
|
|
250
|
+
A table's `where` input also exposes its relations, so you can filter rows by what they're
|
|
251
|
+
related to instead of pulling everything and filtering client-side.
|
|
252
|
+
|
|
253
|
+
A **to-one** relation takes the target table's filter input directly:
|
|
254
|
+
|
|
255
|
+
```graphql
|
|
256
|
+
{
|
|
257
|
+
posts(where: { author: { name: { eq: "FifthUser" } } }) {
|
|
258
|
+
id
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
A **to-many** relation takes a `some` / `none` / `every` wrapper
|
|
264
|
+
(`<Target>ListRelationFilter`):
|
|
265
|
+
|
|
266
|
+
```graphql
|
|
267
|
+
{
|
|
268
|
+
# users who wrote at least one post containing "drizzle"
|
|
269
|
+
users(where: { posts: { some: { content: { like: "%drizzle%" } } } }) {
|
|
270
|
+
id
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
# users with no posts at all
|
|
274
|
+
users(where: { posts: { none: {} } }) {
|
|
275
|
+
id
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# users all of whose posts are published (users with no posts match vacuously)
|
|
279
|
+
users(where: { posts: { every: { isPublished: { eq: true } } } }) {
|
|
280
|
+
id
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
- Relation filters compile to correlated `EXISTS` subqueries — the related rows are never
|
|
286
|
+
fetched, and no join duplicates the parent rows
|
|
287
|
+
- They nest arbitrarily (`users(where: { posts: { some: { author: { … } } } })`) and combine
|
|
288
|
+
freely with column filters (implicit `AND`) and with `OR`
|
|
289
|
+
- They're accepted anywhere a filter is — list and single queries, aggregate queries,
|
|
290
|
+
`update`/`delete` mutations, and the `where` argument on a relation field
|
|
291
|
+
- `some: {}` means "at least one related row exists"; `none: {}` means "none exist"
|
|
292
|
+
- Several modes may be given at once and are `AND`ed together
|
|
293
|
+
- A relation whose name collides with a column name is skipped — the column keeps the field
|
|
294
|
+
- Many-to-many relations declared with `.through()` are filterable too: the `EXISTS`
|
|
295
|
+
subquery joins the junction table to the target, so
|
|
296
|
+
`users(where: { roles: { some: { name: { eq: "admin" } } } })` works the same as a direct
|
|
297
|
+
to-many relation
|
|
298
|
+
|
|
299
|
+
## Aggregate queries
|
|
300
|
+
|
|
301
|
+
Every table also gets an aggregate query field — `<tableName>Aggregate` (e.g. `usersAggregate`),
|
|
302
|
+
following the same naming rules as the other generated queries:
|
|
303
|
+
|
|
304
|
+
```graphql
|
|
305
|
+
{
|
|
306
|
+
postsAggregate(where: { authorId: { eq: 1 } }) {
|
|
307
|
+
count
|
|
308
|
+
avg {
|
|
309
|
+
views
|
|
310
|
+
}
|
|
311
|
+
sum {
|
|
312
|
+
views
|
|
313
|
+
}
|
|
314
|
+
min {
|
|
315
|
+
createdAt
|
|
316
|
+
}
|
|
317
|
+
max {
|
|
318
|
+
createdAt
|
|
319
|
+
title
|
|
320
|
+
}
|
|
321
|
+
countNonNull {
|
|
322
|
+
publishedAt
|
|
323
|
+
}
|
|
324
|
+
countDistinct {
|
|
325
|
+
authorId
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
- `count` — number of matching rows (`Int!`)
|
|
332
|
+
- `avg` / `sum` — one nullable `Float` field per numeric column
|
|
333
|
+
- `min` / `max` — one field per orderable column (numbers, strings, enums, dates, bigints),
|
|
334
|
+
typed exactly like that column is in the table's own type
|
|
335
|
+
- `countNonNull` — `Int!` per column: how many matching rows have a non-null value there.
|
|
336
|
+
Every column qualifies, since `count(col)` is valid whatever the type
|
|
337
|
+
- `countDistinct` — `Int!` per column: how many distinct non-null values there are. Limited to
|
|
338
|
+
the same columns as `min` / `max`, because counting distinct values needs an equality operator
|
|
339
|
+
|
|
340
|
+
Columns that have no meaningful ordering — booleans, arrays, JSON, buffers, and geometry —
|
|
341
|
+
are left out of these types, and the `avg` / `sum` / `min` / `max` fields themselves are
|
|
342
|
+
omitted when no column qualifies.
|
|
343
|
+
|
|
344
|
+
The optional `where` argument takes the same filter input as the table's list query, and is
|
|
345
|
+
applied to every aggregate in the selection. All requested aggregates are computed in a
|
|
346
|
+
single `SELECT`, and on an empty result set `count` is `0` while the other values are `null`.
|
|
347
|
+
|
|
348
|
+
Add `groupBy` to get the same numbers one row per group — see [Group by](#group-by).
|
|
349
|
+
|
|
350
|
+
## Group by
|
|
351
|
+
|
|
352
|
+
Every table with aggregates also gets a `<tableName>GroupBy` query (e.g. `postsGroupBy`): the
|
|
353
|
+
same aggregates as `<tableName>Aggregate`, computed once per distinct combination of the
|
|
354
|
+
columns you group by.
|
|
355
|
+
|
|
356
|
+
```graphql
|
|
357
|
+
{
|
|
358
|
+
postsGroupBy(
|
|
359
|
+
groupBy: [authorId, published]
|
|
360
|
+
where: { createdAt: { gte: "2024-01-01T00:00:00Z" } }
|
|
361
|
+
having: { count: { gte: 5 } }
|
|
362
|
+
) {
|
|
363
|
+
group {
|
|
364
|
+
authorId
|
|
365
|
+
published
|
|
366
|
+
}
|
|
367
|
+
count
|
|
368
|
+
avg {
|
|
369
|
+
views
|
|
370
|
+
}
|
|
371
|
+
max {
|
|
372
|
+
createdAt
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
- `groupBy` is required and takes one or more values of the table's `<Type>GroupByColumn`
|
|
379
|
+
enum. It holds the orderable columns plus booleans — anything the database can group on.
|
|
380
|
+
An empty list is an error, and repeated columns are ignored
|
|
381
|
+
- `group` is a `<Type>GroupKeys!` object with one nullable field per groupable column, typed
|
|
382
|
+
like the table's own column. Columns the query did not group by come back as `null`, which
|
|
383
|
+
a column whose grouped value really is `NULL` is indistinguishable from
|
|
384
|
+
- Every other field is the same one `<tableName>Aggregate` returns — `count`, `avg`, `sum`,
|
|
385
|
+
`min`, `max`, `countNonNull`, `countDistinct` — with the identical types and rules
|
|
386
|
+
- `where` filters rows before grouping; `having` filters groups after aggregating
|
|
387
|
+
- The result is `[<Type>GroupBy!]!`, one row per group, in whatever order the database
|
|
388
|
+
returns them. Add your own ordering client-side if you need it
|
|
389
|
+
|
|
390
|
+
`having` mirrors the aggregate selection, with an `AggregateNumberFilter` (`eq`, `ne`, `gt`,
|
|
391
|
+
`gte`, `lt`, `lte`, all `Float`) in place of each value:
|
|
392
|
+
|
|
393
|
+
```graphql
|
|
394
|
+
{
|
|
395
|
+
postsGroupBy(groupBy: [authorId], having: { count: { gt: 3 }, avg: { views: { gte: 100 } } }) {
|
|
396
|
+
group {
|
|
397
|
+
authorId
|
|
398
|
+
}
|
|
399
|
+
count
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
- `having: { count: … }` filters on the row count; `avg` / `sum` / `min` / `max` /
|
|
405
|
+
`countNonNull` / `countDistinct` take one filter per column, over the same columns the
|
|
406
|
+
matching aggregate type exposes
|
|
407
|
+
- Every entry in a `having` is ANDed together
|
|
408
|
+
- A group filter does not need to be in the selection — `having: { count: { gt: 3 } }` works
|
|
409
|
+
whether or not you asked for `count`
|
|
410
|
+
|
|
411
|
+
The whole thing is one `SELECT … GROUP BY … HAVING …`. Set `features: { groupBy: false }` to
|
|
412
|
+
leave these queries (and their `<Type>GroupBy`, `<Type>GroupKeys`, `<Type>GroupByColumn` and
|
|
413
|
+
`<Type>Having` types) out of the schema; turning off `aggregates` removes them too.
|
|
414
|
+
|
|
415
|
+
## Relation aggregates
|
|
416
|
+
|
|
417
|
+
Every to-many relation also gets an `<relationName>Aggregate` field on the parent type, so you
|
|
418
|
+
can count or summarise related rows without fetching them:
|
|
419
|
+
|
|
420
|
+
```graphql
|
|
421
|
+
{
|
|
422
|
+
users {
|
|
423
|
+
id
|
|
424
|
+
postsAggregate {
|
|
425
|
+
count
|
|
426
|
+
}
|
|
427
|
+
publishedPosts: postsAggregate(where: { published: { eq: true } }) {
|
|
428
|
+
count
|
|
429
|
+
max {
|
|
430
|
+
createdAt
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
- The field returns the target table's own `<Type>Aggregate` type — the same one the root
|
|
438
|
+
`<tableName>Aggregate` query returns, so `count` / `avg` / `sum` / `min` / `max` behave
|
|
439
|
+
identically
|
|
440
|
+
- `where` takes the target table's filter input, including [relation filters](#relation-filters),
|
|
441
|
+
and applies only to the related rows
|
|
442
|
+
- A parent with no related rows gets `count: 0` and `null` for every other aggregate
|
|
443
|
+
- To-one relations get no aggregate field — there is nothing to aggregate over
|
|
444
|
+
- The field is skipped if its name would collide with a column or another relation
|
|
445
|
+
|
|
446
|
+
All parents in a selection are aggregated with a single
|
|
447
|
+
`SELECT <fk>, … WHERE <fk> IN (…) GROUP BY <fk>` per request, so `postsAggregate` on a list of
|
|
448
|
+
users is one extra query, not one per user. Differently-aliased selections with different
|
|
449
|
+
`where` arguments are batched separately.
|
|
450
|
+
|
|
451
|
+
## Distinct
|
|
452
|
+
|
|
453
|
+
List queries take a `distinct` argument — a list of columns from the `<Type>DistinctColumn`
|
|
454
|
+
enum. Rows sharing the same combination of those columns collapse to one:
|
|
455
|
+
|
|
456
|
+
```graphql
|
|
457
|
+
{
|
|
458
|
+
# the first post of each author
|
|
459
|
+
posts(distinct: [authorId], orderBy: { createdAt: { direction: asc, priority: 1 } }) {
|
|
460
|
+
id
|
|
461
|
+
authorId
|
|
462
|
+
createdAt
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
- Which row survives each group is decided by `orderBy` — the first one wins. With no
|
|
468
|
+
`orderBy`, that's the lowest primary key
|
|
469
|
+
- `where` is applied **before** rows are collapsed; `limit` and `offset` are applied
|
|
470
|
+
**after**, so `limit: 10` returns ten distinct rows
|
|
471
|
+
- Several columns are treated as one combined key, not as independent ones
|
|
472
|
+
- `distinct` is also available on to-many relation fields, where it means one row per
|
|
473
|
+
distinct value **per parent** — the window is partitioned by the foreign key as well as
|
|
474
|
+
the requested columns
|
|
475
|
+
- `distinct` is not on single queries — those return one row either way
|
|
476
|
+
|
|
477
|
+
It runs as an extra `row_number() over (partition by …)` query that picks the surviving
|
|
478
|
+
rows' primary keys, after which the main query is narrowed to them — so it needs the same
|
|
479
|
+
window-function support as per-parent paginated relations (**PostgreSQL**, **MySQL 8.0+**,
|
|
480
|
+
or **SQLite 3.25+**), and a table with no primary key cannot use it.
|
|
481
|
+
|
|
482
|
+
## Pagination ordering
|
|
483
|
+
|
|
484
|
+
SQL gives no ordering guarantee for a query that has no `ORDER BY`, so paging through an
|
|
485
|
+
unordered result can return the same row twice or skip one entirely. To keep pages stable,
|
|
486
|
+
a query that returns only part of a table is ordered by its primary key when the request
|
|
487
|
+
supplies no `orderBy`:
|
|
488
|
+
|
|
489
|
+
- list queries with `limit` and/or `offset`
|
|
490
|
+
- `<tableName>Single` queries, which are an implicit `limit 1`
|
|
491
|
+
- to-many relation fields with `limit` and/or `offset` (as a tiebreak appended to any
|
|
492
|
+
`orderBy` you do supply, so per-parent slices are deterministic)
|
|
493
|
+
|
|
494
|
+
An explicit `orderBy` always takes precedence, composite primary keys are ordered by every
|
|
495
|
+
key column, and an unpaginated list query is left unordered so no sort is paid for.
|
|
496
|
+
|
|
497
|
+
## Cursor pagination
|
|
498
|
+
|
|
499
|
+
`OFFSET` pagination degrades linearly and shifts under concurrent writes. List queries also
|
|
500
|
+
support keyset pagination: every row returned by a list query exposes an opaque `cursor`
|
|
501
|
+
field, and passing it back as `after` resumes strictly after that row.
|
|
502
|
+
|
|
503
|
+
```graphql
|
|
504
|
+
{
|
|
505
|
+
posts(orderBy: { createdAt: { direction: desc, priority: 1 } }, limit: 10) {
|
|
506
|
+
id
|
|
507
|
+
createdAt
|
|
508
|
+
cursor
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
# next page — same orderBy, plus the last row's cursor
|
|
513
|
+
{
|
|
514
|
+
posts(
|
|
515
|
+
orderBy: { createdAt: { direction: desc, priority: 1 } }
|
|
516
|
+
limit: 10
|
|
517
|
+
after: "eyJvIjogW1siY3JlYXRlZEF0IiwgImRlc2MiXSwgWyJpZCIsICJhc2MiXV0sIC4uLn0"
|
|
518
|
+
) {
|
|
519
|
+
id
|
|
520
|
+
createdAt
|
|
521
|
+
cursor
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
- The cursor encodes the row's position in the query's **total order** — your `orderBy`
|
|
527
|
+
columns plus the primary key as an ascending tiebreak — so pages are stable even when
|
|
528
|
+
the ordered columns aren't unique, and inserts or deletes mid-scroll don't shift the
|
|
529
|
+
window
|
|
530
|
+
- `after` compiles to a keyset predicate built from drizzle's `and`/`or`/`gt`/`lt`
|
|
531
|
+
comparisons (not SQL row-value syntax), so mixed-direction `orderBy` works
|
|
532
|
+
- `NULL`s in ordered columns follow each dialect's native `ORDER BY` placement —
|
|
533
|
+
PostgreSQL sorts them largest (last in `asc`), MySQL and SQLite smallest (first in
|
|
534
|
+
`asc`) — and the predicate matches, so rows with `NULL` in an ordered column are paged
|
|
535
|
+
through, not skipped
|
|
536
|
+
- Dates and bigints round-trip losslessly through the cursor
|
|
537
|
+
- A cursor is only valid under the ordering it was issued for — reusing it with a
|
|
538
|
+
different `orderBy` (or a malformed/corrupted cursor) returns a `GraphQLError`
|
|
539
|
+
- `after` cannot be combined with `distinct`, and a table with no primary key cannot use
|
|
540
|
+
cursor pagination (its `cursor` field resolves to `null`)
|
|
541
|
+
- If a table has a real column named `cursor`, the column wins and the meta field is
|
|
542
|
+
skipped
|
|
543
|
+
|
|
544
|
+
To-many relation fields take `after` and `distinct` too, with the same meaning they have on
|
|
545
|
+
a root list of the same table — a relation field is a list of the target table, so it gets
|
|
546
|
+
that table's whole pagination surface:
|
|
547
|
+
|
|
548
|
+
```graphql
|
|
549
|
+
{
|
|
550
|
+
users {
|
|
551
|
+
id
|
|
552
|
+
posts(orderBy: { createdAt: { direction: desc, priority: 1 } }, limit: 10, after: "…") {
|
|
553
|
+
id
|
|
554
|
+
cursor
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
Drizzle's `with:` clause cannot express either one — keyset needs a predicate over the
|
|
561
|
+
request's own total order, `distinct` needs a window pass, and a `cursor` has to be computed
|
|
562
|
+
from the raw row. So a relation field that is passed `after` or `distinct`, or that selects
|
|
563
|
+
`cursor`, drops out of the eager fetch and resolves through the request-scoped batch loader
|
|
564
|
+
instead, which implements all three. That is still **one query per relation** across every
|
|
565
|
+
parent in the batch (`distinct` adds its key-selection pass, as on a root list) — never one
|
|
566
|
+
per parent. Nothing about the arguments changes; only how many round-trips the request
|
|
567
|
+
costs.
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
## Upsert
|
|
571
|
+
|
|
572
|
+
`features.upsert` adds a pair of mutations per table that insert rows, or update the ones
|
|
573
|
+
that already exist:
|
|
574
|
+
|
|
575
|
+
```graphql
|
|
576
|
+
mutation {
|
|
577
|
+
upsertUsersSingle(values: { id: 1, name: "Dan", email: "dan@example.com" }) {
|
|
578
|
+
id
|
|
579
|
+
name
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
With no `onConflict`, a conflict on the **primary key** overwrites every column the request
|
|
585
|
+
supplied. `onConflict` changes that:
|
|
586
|
+
|
|
587
|
+
```graphql
|
|
588
|
+
mutation {
|
|
589
|
+
upsertUsers(
|
|
590
|
+
values: [
|
|
591
|
+
{ email: "dan@example.com", name: "Dan", visits: 1 }
|
|
592
|
+
{ email: "sam@example.com", name: "Sam", visits: 1 }
|
|
593
|
+
]
|
|
594
|
+
onConflict: {
|
|
595
|
+
target: [email] # must be a unique constraint; defaults to the primary key
|
|
596
|
+
action: UPDATE # or NOTHING, to keep the existing row
|
|
597
|
+
update: [name] # columns to overwrite; defaults to every supplied column
|
|
598
|
+
where: { isConfirmed: { eq: true } } # only overwrite rows that match
|
|
599
|
+
}
|
|
600
|
+
) {
|
|
601
|
+
id
|
|
602
|
+
name
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
- A batch upsert updates each row with **its own** values (`excluded.<column>`), not with
|
|
608
|
+
the last row's
|
|
609
|
+
- Columns the request did not supply are never overwritten — a partial upsert does not null
|
|
610
|
+
out the rest of the row. Listing an unsupplied column in `update` is an error rather than
|
|
611
|
+
a silent no-op
|
|
612
|
+
- `target` must match one of the table's unique constraints exactly; anything else is
|
|
613
|
+
rejected with the list of valid targets, instead of the database's opaque "no unique or
|
|
614
|
+
exclusion constraint matching" error
|
|
615
|
+
- `action: NOTHING` inserts nothing and returns nothing for the conflicting row. An `UPDATE`
|
|
616
|
+
with no columns left to write degrades to the same thing
|
|
617
|
+
- `values` is the same `Create<Type>Input` the insert mutations take, so turning `insert`
|
|
618
|
+
off does not remove it
|
|
619
|
+
|
|
620
|
+
Dialect differences:
|
|
621
|
+
|
|
622
|
+
- **PostgreSQL** and **SQLite** — the full surface above. A table with no primary key and no
|
|
623
|
+
unique constraint has nothing to conflict on, so it gets no upsert mutations at all
|
|
624
|
+
- **MySQL** — `ON DUPLICATE KEY UPDATE` fires on whichever unique key was violated and takes
|
|
625
|
+
no predicate, so `<Type>OnConflict` there has only `action` and `update`. `action: NOTHING`
|
|
626
|
+
becomes `INSERT IGNORE`, and the mutations return `MutationReturn` like every other MySQL
|
|
627
|
+
mutation
|
|
628
|
+
|
|
629
|
+
The build-wide `conflictDoNothing` option is deprecated in favour of this: it applies to every
|
|
630
|
+
`create*` mutation with no way for a request to opt out. `onConflict: { action: NOTHING }` is
|
|
631
|
+
the per-request replacement.
|
|
632
|
+
|
|
633
|
+
## Nested writes
|
|
634
|
+
|
|
635
|
+
`features.nestedWrites` adds a field per writable relation to every create and update input,
|
|
636
|
+
so a row and the rows it is related to can be written in one mutation:
|
|
637
|
+
|
|
638
|
+
```graphql
|
|
639
|
+
mutation {
|
|
640
|
+
createPostsSingle(
|
|
641
|
+
values: {
|
|
642
|
+
title: "Hello"
|
|
643
|
+
author: { connect: { email: { eq: "dan@example.com" } } }
|
|
644
|
+
comments: { create: [{ body: "First" }, { body: "Second" }] }
|
|
645
|
+
}
|
|
646
|
+
) {
|
|
647
|
+
id
|
|
648
|
+
author {
|
|
649
|
+
name
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
Every relation field offers `create` and `connect`; on the update input, a relation whose
|
|
656
|
+
foreign key is nullable also offers `disconnect` and — for a to-many relation — `set`:
|
|
657
|
+
|
|
658
|
+
```graphql
|
|
659
|
+
mutation {
|
|
660
|
+
updateUsersSingle(
|
|
661
|
+
where: { id: { eq: 1 } }
|
|
662
|
+
set: { posts: { set: [{ isPublished: { eq: true } }], create: [{ title: "New" }] } }
|
|
663
|
+
) {
|
|
664
|
+
id
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
- `connect`, `disconnect` and `set` take the target's own `Filters` input, so they select
|
|
670
|
+
rows the same way a query does. A filter that matches everything is rejected rather than
|
|
671
|
+
attaching the whole table
|
|
672
|
+
- `set` replaces the relation wholesale — everything attached is detached first, then the
|
|
673
|
+
filters attach. It runs before `create`, so a row created in the same write survives it
|
|
674
|
+
- A to-one relation takes one operation at a time (`connect` **or** `disconnect` **or**
|
|
675
|
+
`create`), and attaching to it detaches whatever was there
|
|
676
|
+
- `disconnect` and `set` only exist where the foreign key is nullable; there is no way to
|
|
677
|
+
detach a row whose key is `NOT NULL`
|
|
678
|
+
- A column the relation fills in becomes nullable on the create input — with
|
|
679
|
+
`author: { create: … }` available, `authorId` is one of two ways to supply it
|
|
680
|
+
- Nesting is one level deep: the row a nested `create` inserts takes columns only, and the
|
|
681
|
+
join column is left off, since the write it is part of sets it
|
|
682
|
+
- Many-to-many (`.through()`) relations are not written through
|
|
683
|
+
- The whole tree runs in one transaction — a savepoint when the request already carries one
|
|
684
|
+
— so a failure anywhere leaves nothing behind
|
|
685
|
+
- A `set` that carries only relation fields is a valid update: it changes what the row is
|
|
686
|
+
attached to without rewriting the row
|
|
687
|
+
|
|
688
|
+
Dialect support: **PostgreSQL**, and **SQLite** on an asynchronous driver (libsql, D1). A
|
|
689
|
+
nested write reads back the key of the row it just wrote, which MySQL has no `RETURNING` for,
|
|
690
|
+
and interleaves awaited statements inside a transaction, which a synchronous SQLite driver
|
|
691
|
+
cannot do — `buildSchema` rejects both combinations rather than generating something that
|
|
692
|
+
half-works.
|
|
693
|
+
|
|
694
|
+
## Atomic column updates
|
|
695
|
+
|
|
696
|
+
`Update<Table>Input` sets columns to values, so "increment the view count" is a read followed
|
|
697
|
+
by a write of the number the read returned — two round-trips, and a lost update whenever two
|
|
698
|
+
clients read the same number. `features.fieldUpdateOperations` replaces qualifying columns'
|
|
699
|
+
update fields with an operations input, so the arithmetic happens in the database:
|
|
700
|
+
|
|
701
|
+
```graphql
|
|
702
|
+
mutation {
|
|
703
|
+
updatePostsSingle(where: { id: { eq: 1 } }, set: { views: { increment: 1 } }) {
|
|
704
|
+
views
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
compiles to `SET views = views + 1`. The flag changes the update inputs only; insert inputs
|
|
710
|
+
are untouched.
|
|
711
|
+
|
|
712
|
+
- **Numeric columns** (`Int`, `Float`, `BigInt`, `Decimal`) take
|
|
713
|
+
`set` / `increment` / `decrement` / `multiply` / `divide`
|
|
714
|
+
- **Array columns** take `set` / `push`, where `push` appends to the existing array
|
|
715
|
+
- Exactly one operation per column. Passing two, or `{}`, is a `GraphQLError` naming the
|
|
716
|
+
column — graphql-js has no `@oneOf` on the version range this library supports, so it is
|
|
717
|
+
enforced at resolve time rather than by the type
|
|
718
|
+
- `set` is the explicit spelling of the plain assignment the field had before the flag, so
|
|
719
|
+
every existing update has a one-key rewrite: `{ views: 5 }` → `{ views: { set: 5 } }`
|
|
720
|
+
- The input types are named for the **scalar**, not the column — every `Int` column in the
|
|
721
|
+
schema shares one `IntFieldUpdate` — and a column claimed by a `scalarOverrides` entry
|
|
722
|
+
keeps its plain field, since the override decides what that column accepts
|
|
723
|
+
|
|
724
|
+
Two things follow the database's own semantics rather than being guarded:
|
|
725
|
+
|
|
726
|
+
- `increment`/`multiply` on an integer column can overflow, and `divide` on one **rounds**
|
|
727
|
+
the way the dialect rounds (PostgreSQL truncates toward zero; use a `Float` or `Decimal`
|
|
728
|
+
column if you need the fraction)
|
|
729
|
+
- `divide: 0` raises the driver's division-by-zero error, surfaced as a `GraphQLError`
|
|
730
|
+
|
|
731
|
+
## Row counts instead of rows
|
|
732
|
+
|
|
733
|
+
The plural `update<Table>` / `delete<Table>` mutations always `RETURNING *`, which is wasted
|
|
734
|
+
work — and wasted bandwidth — when a client only wants to know how many rows were touched.
|
|
735
|
+
`features.countMutations` adds a count-returning variant of each:
|
|
736
|
+
|
|
737
|
+
```graphql
|
|
738
|
+
mutation {
|
|
739
|
+
deletePostsCount(where: { status: { eq: "spam" } })
|
|
740
|
+
updatePostsCount(where: { draft: { eq: true } }, set: { draft: false })
|
|
741
|
+
}
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
- Both return `Int!` — the number of rows the write affected — and add no new output type
|
|
745
|
+
to the schema
|
|
746
|
+
- Arguments are exactly the plural mutation's, so a client swaps one field name for the
|
|
747
|
+
other; `features.requireWhere` applies to them the same way
|
|
748
|
+
- They follow the feature switch of the write they mirror: `updateCount` needs
|
|
749
|
+
`features.update`, `deleteCount` needs `features.delete`
|
|
750
|
+
- **MySQL** gets them too, and they are arguably most useful there: MySQL's other mutations
|
|
751
|
+
return only `MutationReturn { isSuccess }`, so a row count is strictly more information
|
|
752
|
+
- `update<Table>Count` **rejects** nested writes with a `GraphQLError` — a nested write
|
|
753
|
+
needs the parent rows this mutation deliberately does not read back. Use the plural
|
|
754
|
+
mutation for those
|
|
755
|
+
- The count is read from whichever field the driver reports it in (`rowCount`,
|
|
756
|
+
`affectedRows`, `rowsAffected`, `changes`, `count`); a driver that reports none is a
|
|
757
|
+
`GraphQLError` rather than a silent `0`
|
|
758
|
+
|
|
759
|
+
## Single-row update & delete
|
|
760
|
+
|
|
761
|
+
Alongside the plural `update<Table>` / `delete<Table>` mutations, every table gets an
|
|
762
|
+
`update<Table>Single` / `delete<Table>Single` variant that targets exactly one row:
|
|
763
|
+
|
|
764
|
+
```graphql
|
|
765
|
+
mutation {
|
|
766
|
+
updateUsersSingle(where: { id: { eq: 1 } }, set: { name: "Dan" }) {
|
|
767
|
+
id
|
|
768
|
+
name
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
- `where` is **non-null** and must contain at least one filter — `where: {}` is rejected at
|
|
774
|
+
resolve time, so a Single write can never become an unbounded one
|
|
775
|
+
- The return type is the single (nullable) row type, not a list: the affected row comes back
|
|
776
|
+
directly, and **no match returns `null`** instead of an empty list
|
|
777
|
+
- If `where` matches **more than one row**, the mutation throws a `GraphQLError` and writes
|
|
778
|
+
nothing — the match is checked (`LIMIT 2`) before the write runs
|
|
779
|
+
- **MySQL** cannot return the affected row (no `RETURNING`), so its Single variants keep the
|
|
780
|
+
single-match and non-empty-`where` guarantees but return `MutationReturn` like every other
|
|
781
|
+
MySQL mutation
|
|
782
|
+
- The variants are generated under `features.update` / `features.delete`, share the plural
|
|
783
|
+
mutations' input types, and appear in `entities.mutations` for custom schemas
|
|
784
|
+
|
|
785
|
+
### Mutation return shapes
|
|
786
|
+
|
|
787
|
+
Every mutation that returns rows returns them non-null, with two exceptions that are
|
|
788
|
+
deliberate rather than accidental:
|
|
789
|
+
|
|
790
|
+
| Mutation | Returns | Why |
|
|
791
|
+
| --- | --- | --- |
|
|
792
|
+
| `create<Table>Single` | `Table!`, or `Table` under `conflictDoNothing` | An insert of one row returns that row. It can only fail to produce one when the `conflictDoNothing` build option swallows a conflict, so it is nullable exactly when that option is set |
|
|
793
|
+
| `update<Table>Many` | `[Table]!` | Each entry's updated rows **in entry order**. An entry whose `where` matched nothing contributes `null` in its slot, so the response lines up with the request positionally — the nullable element is the whole point |
|
|
794
|
+
| everything else | `[Table!]!` / `Table!` / `Table` | — |
|
|
795
|
+
|
|
796
|
+
The plural mutations still accept a missing `where` (a full-table write) by default. To rule
|
|
797
|
+
that out at the type level, `features: { requireWhere: true }` makes `where` non-null on
|
|
798
|
+
`update<Table>` / `delete<Table>` and rejects a `where` with no filters, exactly like the
|
|
799
|
+
Single variants. It defaults to `false` for backwards compatibility.
|
|
800
|
+
|
|
801
|
+
## Query cost
|
|
802
|
+
|
|
803
|
+
Generated fields carry a `complexity` hint in their GraphQL extensions, ready for
|
|
804
|
+
[`graphql-query-complexity`](https://github.com/slicknode/graphql-query-complexity)'s
|
|
805
|
+
`fieldExtensionsEstimator`. The generator knows which fields are paginated and which are
|
|
806
|
+
aggregates, so the cost tracks the rows a query can actually pull rather than the number of
|
|
807
|
+
fields it mentions:
|
|
808
|
+
|
|
809
|
+
| Field | Cost |
|
|
810
|
+
| --- | --- |
|
|
811
|
+
| List query, to-many relation | `(limit ?? defaultListSize) * childComplexity` |
|
|
812
|
+
| Aggregate query, group-by query, `<relation>Aggregate` | `aggregateCost + childComplexity` |
|
|
813
|
+
| Everything else | no hint — your estimator's default applies |
|
|
814
|
+
|
|
815
|
+
So `users(limit: 20) { id posts(limit: 5) { id } }` costs `20 * (1 + 5 * 1)` = 120, while the
|
|
816
|
+
same query without limits costs `10 * (1 + 10 * 1)` = 110.
|
|
817
|
+
|
|
818
|
+
The hints do nothing until you install a complexity rule, so they are generated by default:
|
|
819
|
+
|
|
820
|
+
```ts
|
|
821
|
+
import { createComplexityRule, fieldExtensionsEstimator, simpleEstimator } from 'graphql-query-complexity';
|
|
822
|
+
|
|
823
|
+
const rule = createComplexityRule({
|
|
824
|
+
maximumComplexity: 1000,
|
|
825
|
+
estimators: [fieldExtensionsEstimator(), simpleEstimator({ defaultComplexity: 1 })],
|
|
826
|
+
});
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
Tune the two assumptions, or turn the hints off entirely:
|
|
830
|
+
|
|
831
|
+
```ts
|
|
832
|
+
buildSchema(db, {
|
|
833
|
+
complexity: { defaultListSize: 25, aggregateCost: 50 },
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
buildSchema(db, { complexity: false });
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
### Depth
|
|
840
|
+
|
|
841
|
+
Cost is not a depth bound. A cyclic relation graph (`user -> posts -> author -> posts -> …`)
|
|
842
|
+
lets a client nest as deep as it likes, and a deep query over cheap fields can stay under any
|
|
843
|
+
complexity ceiling. Put a depth limit in front of a publicly exposed schema as well — e.g.
|
|
844
|
+
[`graphql-depth-limit`](https://github.com/stems/graphql-depth-limit) — or set
|
|
845
|
+
`relationsDepthLimit: 0` to generate no relation fields at all.
|
|
846
|
+
|
|
847
|
+
## Error handling
|
|
848
|
+
|
|
849
|
+
Database drivers put a lot into an error message. Drizzle rethrows them with the full SQL
|
|
850
|
+
statement and its bound parameters attached, and Postgres itself names the table, the column
|
|
851
|
+
and the constraint that was violated. None of that belongs in a GraphQL response, so by
|
|
852
|
+
default every error a generated resolver throws is passed through a sanitizer:
|
|
853
|
+
|
|
854
|
+
- errors drizzle-graphql raises itself (`Unable to update with no values specified!`,
|
|
855
|
+
`Field 'x' is not a valid date!`, filter misuse, …) are written for the client and pass
|
|
856
|
+
through unchanged
|
|
857
|
+
- anything else becomes `Internal server error` with `extensions.code:
|
|
858
|
+
"INTERNAL_SERVER_ERROR"`, and the original is kept on the error's `originalError` so a
|
|
859
|
+
server-side logger can still see it
|
|
860
|
+
|
|
861
|
+
`onError` overrides this. Return an error to surface that one, or return nothing to let the
|
|
862
|
+
default apply — which makes it a pure logging hook:
|
|
863
|
+
|
|
864
|
+
```Typescript
|
|
865
|
+
const { schema } = buildSchema(db, {
|
|
866
|
+
onError: (error) => {
|
|
867
|
+
logger.error({ err: error }, 'drizzle-graphql resolver failed')
|
|
868
|
+
// no return value — the default sanitizing still applies
|
|
869
|
+
},
|
|
870
|
+
})
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
```Typescript
|
|
874
|
+
// Surface raw database errors, e.g. in development
|
|
875
|
+
buildSchema(db, { onError: (error) => error as Error })
|
|
876
|
+
|
|
877
|
+
// Or map them yourself
|
|
878
|
+
buildSchema(db, {
|
|
879
|
+
onError: (error) =>
|
|
880
|
+
isUniqueViolation(error)
|
|
881
|
+
? new GraphQLError('That record already exists', { extensions: { code: 'CONFLICT' } })
|
|
882
|
+
: undefined,
|
|
883
|
+
})
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
The hook covers root queries and mutations, relation and aggregate fields, and the
|
|
887
|
+
standalone `entities.fieldResolvers`. The default is exported as `defaultErrorMapper` if you
|
|
888
|
+
want to fall back to it explicitly.
|
|
889
|
+
|
|
890
|
+
## Transactions
|
|
891
|
+
|
|
892
|
+
Each resolver runs its statements on the database the schema was built from. A request that
|
|
893
|
+
fires several mutations therefore commits each one separately, and a failure halfway leaves
|
|
894
|
+
the earlier ones in place. To run a whole request as one unit, open a transaction yourself
|
|
895
|
+
and put it on the GraphQL context under the exported `drizzleExecutorKey`:
|
|
896
|
+
|
|
897
|
+
```Typescript
|
|
898
|
+
import { buildSchema, drizzleExecutorKey } from 'drizzle-graphql'
|
|
899
|
+
|
|
900
|
+
const { schema } = buildSchema(db)
|
|
901
|
+
|
|
902
|
+
await db.transaction(async (tx) => {
|
|
903
|
+
const result = await graphql({
|
|
904
|
+
schema,
|
|
905
|
+
source: request.query,
|
|
906
|
+
variableValues: request.variables,
|
|
907
|
+
contextValue: { [drizzleExecutorKey]: tx },
|
|
908
|
+
})
|
|
909
|
+
|
|
910
|
+
if (result.errors?.length) throw new Error('rolling back')
|
|
911
|
+
return result
|
|
912
|
+
})
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
Every generated resolver reads the key at resolve time, so queries, mutations, aggregates
|
|
916
|
+
and relation field resolvers all run on the executor you supply and see its uncommitted
|
|
917
|
+
rows. With no key on the context, everything falls back to the build-time database, which
|
|
918
|
+
is what an ordinary request does.
|
|
919
|
+
|
|
920
|
+
The value does not have to be a transaction — any object with the same interface works, for
|
|
921
|
+
example a pooled connection bound to a tenant, or a logging proxy around `db`. Because the
|
|
922
|
+
key is created with `Symbol.for`, the ESM and CJS builds of this package agree on it when
|
|
923
|
+
both end up loaded in one process.
|
|
924
|
+
|
|
925
|
+
## Relations & N+1 handling
|
|
926
|
+
|
|
927
|
+
Generated schemas resolve nested relations without N+1 query explosions:
|
|
928
|
+
|
|
929
|
+
- **Queries** — root queries (`entities.queries.*`) eagerly load every requested
|
|
930
|
+
relation in a single round-trip using Drizzle's relational query builder
|
|
931
|
+
(`with:`), including nested relations and per-relation `where` / `orderBy` /
|
|
932
|
+
`limit` / `offset` arguments.
|
|
933
|
+
- **Mutations (PostgreSQL & SQLite)** — after an insert or update, if the selection set
|
|
934
|
+
includes relation fields, the affected rows are re-fetched by primary key through one
|
|
935
|
+
relational query so their relations are eagerly loaded (single- and composite-column
|
|
936
|
+
primary keys are supported). `delete` mutations keep using the request-scoped batch
|
|
937
|
+
loader, since the rows no longer exist to re-fetch. MySQL mutations return only a
|
|
938
|
+
success flag (no row payload), so this step does not apply there.
|
|
939
|
+
- **Custom schemas** — each relation field also has a standalone resolver, exported as
|
|
940
|
+
`entities.fieldResolvers[TableName][relationName]`. When you wire generated types
|
|
941
|
+
into your own schema and your root resolver does **not** pre-fetch relations, these
|
|
942
|
+
resolvers batch all sibling loads in a request into a single `IN (…)` query
|
|
943
|
+
(request-scoped, keyed on the GraphQL `context`), preventing N+1. Per-parent
|
|
944
|
+
`limit` / `offset` on a to-many relation is applied across the whole batch using a
|
|
945
|
+
`ROW_NUMBER() OVER (PARTITION BY …)` window function — still one query, not one per
|
|
946
|
+
parent.
|
|
947
|
+
|
|
948
|
+
```Typescript
|
|
949
|
+
// entities.fieldResolvers is keyed by table name, then relation name
|
|
950
|
+
const usersPostsResolver = entities.fieldResolvers.Users.posts
|
|
951
|
+
```
|
|
952
|
+
|
|
953
|
+
> [!IMPORTANT]
|
|
954
|
+
> Per-parent paginated relations (a to-many relation field with `limit` or `offset`)
|
|
955
|
+
> rely on SQL window functions. These require **PostgreSQL**, **MySQL 8.0+**, or
|
|
956
|
+
> **SQLite 3.25+**. Relations without pagination, and all other query/mutation paths,
|
|
957
|
+
> have no such requirement.
|
|
958
|
+
|
|
959
|
+
### Relation nullability
|
|
960
|
+
|
|
961
|
+
A **to-many** relation field is always `[Target!]!`. A **to-one** relation field is
|
|
962
|
+
nullable by default, but honors the relation's declared optionality: declaring
|
|
963
|
+
`optional: false` on a Drizzle `one` relation — the assertion that the related row always
|
|
964
|
+
exists, i.e. a `NOT NULL` foreign key — emits the field as `Target!`:
|
|
965
|
+
|
|
966
|
+
```Typescript
|
|
967
|
+
const relations = buildRelations({ Users, Posts }, {
|
|
968
|
+
Posts: {
|
|
969
|
+
// posts.author_id is NOT NULL → author: Users!
|
|
970
|
+
author: r.one.Users({ from: r.Posts.authorId, to: r.Users.id, optional: false }),
|
|
971
|
+
},
|
|
972
|
+
})
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
Only the explicit `optional: false` declaration is honored — required-ness is never
|
|
976
|
+
inferred from column nullability, since a `NOT NULL` `from` column does not guarantee a
|
|
977
|
+
related row exists when the constraint lives on the other side of the join. Note that on a
|
|
978
|
+
required relation, a `where` argument that filters out the related row (or a dangling
|
|
979
|
+
foreign key) resolves to `null` and therefore surfaces as a GraphQL non-null error.
|
|
980
|
+
|
|
981
|
+
### Overriding a relation's resolver without overfetching
|
|
982
|
+
|
|
983
|
+
By default the eager `with:` pre-fetch is driven purely by the GraphQL selection set:
|
|
984
|
+
any selected relation is fetched from the database, even if you intend to resolve it
|
|
985
|
+
yourself (from a cache, another service, a dataloader, …). To override a relation's
|
|
986
|
+
resolver, first opt it out of eager loading with `eagerLoadRelations` so the parent
|
|
987
|
+
query stops fetching it, then supply your resolver with the standard
|
|
988
|
+
[`@graphql-tools/schema`](https://the-guild.dev/graphql/tools) utilities:
|
|
989
|
+
|
|
990
|
+
```Typescript
|
|
991
|
+
import { addResolversToSchema } from '@graphql-tools/schema'
|
|
992
|
+
|
|
993
|
+
const { schema } = buildSchema(db, {
|
|
994
|
+
// Exclude Users.posts from the parent query's `with:` clause.
|
|
995
|
+
// Other relations keep eager-loading as usual.
|
|
996
|
+
eagerLoadRelations: (table, relation) => !(table === 'Users' && relation === 'posts'),
|
|
997
|
+
})
|
|
998
|
+
|
|
999
|
+
const finalSchema = addResolversToSchema({
|
|
1000
|
+
schema,
|
|
1001
|
+
resolvers: {
|
|
1002
|
+
Users: {
|
|
1003
|
+
posts: (parent, args, context) => context.postsLoader.load(parent.id),
|
|
1004
|
+
},
|
|
1005
|
+
},
|
|
1006
|
+
})
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
`eagerLoadRelations` accepts:
|
|
1010
|
+
|
|
1011
|
+
- `true` (default) — eager-load every relation.
|
|
1012
|
+
- `false` — never eager-load; every relation resolves lazily through its
|
|
1013
|
+
(request-batched) field resolver.
|
|
1014
|
+
- `(tableName, relationName) => boolean` — decide per relation. Returning `false`
|
|
1015
|
+
excludes that relation from `with:` (and from the mutation eager re-fetch).
|
|
1016
|
+
|
|
1017
|
+
Opting a relation out does **not** remove its field — it keeps resolving lazily via the
|
|
1018
|
+
request-scoped batch loader, so the field still works even before you override it. Table
|
|
1019
|
+
and relation names are the Drizzle schema keys (e.g. `Users`, `posts`), matching the keys
|
|
1020
|
+
of `entities.fieldResolvers`.
|