@pothos/plugin-drizzle 0.18.1 → 0.19.1
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/CHANGELOG.md +357 -0
- package/README.md +1199 -563
- package/dts/connection-helpers.d.ts +18 -8
- package/dts/connection-helpers.d.ts.map +1 -1
- package/dts/drizzle-field-builder.d.ts +4 -11
- package/dts/drizzle-field-builder.d.ts.map +1 -1
- package/dts/global-types.d.ts.map +1 -1
- package/dts/index.d.ts.map +1 -1
- package/dts/model-loader.d.ts +18 -17
- package/dts/model-loader.d.ts.map +1 -1
- package/dts/types.d.ts +82 -22
- package/dts/types.d.ts.map +1 -1
- package/dts/utils/adapter.d.ts +39 -0
- package/dts/utils/adapter.d.ts.map +1 -0
- package/dts/utils/await-selections.d.ts +12 -0
- package/dts/utils/await-selections.d.ts.map +1 -0
- package/dts/utils/config.d.ts +23 -3
- package/dts/utils/config.d.ts.map +1 -1
- package/dts/utils/cursors.d.ts +6 -6
- package/dts/utils/cursors.d.ts.map +1 -1
- package/dts/utils/map-query.d.ts +22 -21
- package/dts/utils/map-query.d.ts.map +1 -1
- package/dts/utils/path-info.d.ts +13 -0
- package/dts/utils/path-info.d.ts.map +1 -0
- package/dts/utils/relation-filter.d.ts +48 -0
- package/dts/utils/relation-filter.d.ts.map +1 -0
- package/dts/utils/selections.d.ts +2 -21
- package/dts/utils/selections.d.ts.map +1 -1
- package/esm/connection-helpers.d.ts +18 -8
- package/esm/connection-helpers.d.ts.map +1 -1
- package/esm/connection-helpers.js +52 -19
- package/esm/connection-helpers.js.map +1 -1
- package/esm/drizzle-field-builder.d.ts +4 -11
- package/esm/drizzle-field-builder.d.ts.map +1 -1
- package/esm/drizzle-field-builder.js +138 -96
- package/esm/drizzle-field-builder.js.map +1 -1
- package/esm/field-builder.js +61 -47
- package/esm/field-builder.js.map +1 -1
- package/esm/global-types.d.ts.map +1 -1
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +37 -61
- package/esm/index.js.map +1 -1
- package/esm/model-loader.d.ts +18 -17
- package/esm/model-loader.d.ts.map +1 -1
- package/esm/model-loader.js +76 -45
- package/esm/model-loader.js.map +1 -1
- package/esm/schema-builder.js +40 -149
- package/esm/schema-builder.js.map +1 -1
- package/esm/types.d.ts +82 -22
- package/esm/types.d.ts.map +1 -1
- package/esm/types.js.map +1 -1
- package/esm/utils/adapter.d.ts +39 -0
- package/esm/utils/adapter.d.ts.map +1 -0
- package/esm/utils/adapter.js +137 -0
- package/esm/utils/adapter.js.map +1 -0
- package/esm/utils/await-selections.d.ts +12 -0
- package/esm/utils/await-selections.d.ts.map +1 -0
- package/esm/utils/await-selections.js +19 -0
- package/esm/utils/await-selections.js.map +1 -0
- package/esm/utils/config.d.ts +23 -3
- package/esm/utils/config.d.ts.map +1 -1
- package/esm/utils/config.js +1 -4
- package/esm/utils/config.js.map +1 -1
- package/esm/utils/cursors.d.ts +6 -6
- package/esm/utils/cursors.d.ts.map +1 -1
- package/esm/utils/cursors.js +75 -92
- package/esm/utils/cursors.js.map +1 -1
- package/esm/utils/map-query.d.ts +22 -21
- package/esm/utils/map-query.d.ts.map +1 -1
- package/esm/utils/map-query.js +40 -354
- package/esm/utils/map-query.js.map +1 -1
- package/esm/utils/path-info.d.ts +13 -0
- package/esm/utils/path-info.d.ts.map +1 -0
- package/esm/utils/path-info.js +37 -0
- package/esm/utils/path-info.js.map +1 -0
- package/esm/utils/relation-filter.d.ts +48 -0
- package/esm/utils/relation-filter.d.ts.map +1 -0
- package/esm/utils/relation-filter.js +92 -0
- package/esm/utils/relation-filter.js.map +1 -0
- package/esm/utils/selections.d.ts +2 -21
- package/esm/utils/selections.d.ts.map +1 -1
- package/esm/utils/selections.js +1 -133
- package/esm/utils/selections.js.map +1 -1
- package/lib/connection-helpers.js +51 -18
- package/lib/connection-helpers.js.map +1 -1
- package/lib/drizzle-field-builder.js +136 -94
- package/lib/drizzle-field-builder.js.map +1 -1
- package/lib/field-builder.js +58 -44
- package/lib/field-builder.js.map +1 -1
- package/lib/index.js +36 -60
- package/lib/index.js.map +1 -1
- package/lib/model-loader.js +76 -45
- package/lib/model-loader.js.map +1 -1
- package/lib/schema-builder.js +40 -149
- package/lib/schema-builder.js.map +1 -1
- package/lib/types.js.map +1 -1
- package/lib/utils/adapter.js +147 -0
- package/lib/utils/adapter.js.map +1 -0
- package/lib/utils/await-selections.js +20 -0
- package/lib/utils/await-selections.js.map +1 -0
- package/lib/utils/config.js +1 -4
- package/lib/utils/config.js.map +1 -1
- package/lib/utils/cursors.js +67 -90
- package/lib/utils/cursors.js.map +1 -1
- package/lib/utils/map-query.js +28 -359
- package/lib/utils/map-query.js.map +1 -1
- package/lib/utils/path-info.js +39 -0
- package/lib/utils/path-info.js.map +1 -0
- package/lib/utils/relation-filter.js +91 -0
- package/lib/utils/relation-filter.js.map +1 -0
- package/lib/utils/selections.js +3 -158
- package/lib/utils/selections.js.map +1 -1
- package/package.json +10 -8
- package/src/connection-helpers.ts +168 -54
- package/src/drizzle-field-builder.ts +296 -176
- package/src/field-builder.ts +110 -77
- package/src/global-types.ts +0 -32
- package/src/index.ts +42 -68
- package/src/model-loader.ts +110 -71
- package/src/schema-builder.ts +76 -169
- package/src/types.ts +178 -46
- package/src/utils/adapter.ts +197 -0
- package/src/utils/await-selections.ts +31 -0
- package/src/utils/config.ts +1 -5
- package/src/utils/cursors.ts +84 -83
- package/src/utils/map-query.ts +51 -734
- package/src/utils/path-info.ts +40 -0
- package/src/utils/relation-filter.ts +185 -0
- package/src/utils/selections.ts +2 -207
- package/dts/utils/deep-equal.d.ts +0 -2
- package/dts/utils/deep-equal.d.ts.map +0 -1
- package/dts/utils/drizzle-graphql.d.ts +0 -2
- package/dts/utils/drizzle-graphql.d.ts.map +0 -1
- package/dts/utils/loader-map.d.ts +0 -11
- package/dts/utils/loader-map.d.ts.map +0 -1
- package/dts/utils/usage.d.ts +0 -5
- package/dts/utils/usage.d.ts.map +0 -1
- package/esm/utils/deep-equal.d.ts +0 -2
- package/esm/utils/deep-equal.d.ts.map +0 -1
- package/esm/utils/deep-equal.js +0 -45
- package/esm/utils/deep-equal.js.map +0 -1
- package/esm/utils/drizzle-graphql.d.ts +0 -2
- package/esm/utils/drizzle-graphql.d.ts.map +0 -1
- package/esm/utils/drizzle-graphql.js +0 -232
- package/esm/utils/drizzle-graphql.js.map +0 -1
- package/esm/utils/loader-map.d.ts +0 -11
- package/esm/utils/loader-map.d.ts.map +0 -1
- package/esm/utils/loader-map.js +0 -35
- package/esm/utils/loader-map.js.map +0 -1
- package/esm/utils/usage.d.ts +0 -5
- package/esm/utils/usage.d.ts.map +0 -1
- package/esm/utils/usage.js +0 -59
- package/esm/utils/usage.js.map +0 -1
- package/lib/utils/deep-equal.js +0 -55
- package/lib/utils/deep-equal.js.map +0 -1
- package/lib/utils/drizzle-graphql.js +0 -236
- package/lib/utils/drizzle-graphql.js.map +0 -1
- package/lib/utils/loader-map.js +0 -56
- package/lib/utils/loader-map.js.map +0 -1
- package/lib/utils/usage.js +0 -83
- package/lib/utils/usage.js.map +0 -1
- package/src/utils/deep-equal.ts +0 -57
- package/src/utils/drizzle-graphql.ts +0 -274
- package/src/utils/loader-map.ts +0 -49
- package/src/utils/usage.ts +0 -62
package/README.md
CHANGED
|
@@ -1,81 +1,162 @@
|
|
|
1
|
-
# Drizzle Plugin
|
|
1
|
+
# Drizzle Plugin for Pothos
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Note:**
|
|
4
|
+
This plugin uses drizzle's [relational query builder v2](https://orm.drizzle.team/docs/rqb-v2),
|
|
5
|
+
which ships in `drizzle-orm` 1.0. That release is still a release candidate, and npm's `latest`
|
|
6
|
+
tag is the 0.x line, so install drizzle with the `rc` tag. Its API can still change before 1.0 is
|
|
7
|
+
final.
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
If you are upgrading from an older version of this plugin, read drizzle's [relations v1 to v2
|
|
10
|
+
guide](https://orm.drizzle.team/docs/relations-v1-v2), then this package's changelog for the
|
|
11
|
+
Pothos specific changes.
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
The Drizzle plugin maps database tables to GraphQL types and plans database queries from the
|
|
15
|
+
fields a client selects. It supports relations, computed fields, and Relay nodes and connections.
|
|
16
|
+
|
|
17
|
+
The examples use authors and posts to demonstrate the plugin APIs.
|
|
18
|
+
|
|
19
|
+
### Getting started
|
|
20
|
+
|
|
21
|
+
[Setup](https://pothos-graphql.dev/docs/plugins/drizzle/setup) connects your Drizzle client to Pothos.
|
|
22
|
+
[Objects](https://pothos-graphql.dev/docs/plugins/drizzle/objects) covers type definitions and fields such as this lookup:
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
builder.queryType({
|
|
26
|
+
fields: (t) => ({
|
|
27
|
+
author: t.drizzleField({
|
|
28
|
+
type: 'users',
|
|
29
|
+
nullable: true,
|
|
30
|
+
args: { id: t.arg.int({ required: true }) },
|
|
31
|
+
resolve: (query, _root, args) => {
|
|
32
|
+
return db.query.users.findFirst(
|
|
33
|
+
query({
|
|
34
|
+
where: { id: args.id },
|
|
35
|
+
}),
|
|
36
|
+
);
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
}),
|
|
40
|
+
});
|
|
7
41
|
```
|
|
8
42
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
43
|
+
```graphql
|
|
44
|
+
query AuthorPage {
|
|
45
|
+
author(id: 1) {
|
|
46
|
+
fullName
|
|
47
|
+
posts {
|
|
48
|
+
title
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
12
53
|
|
|
13
|
-
|
|
14
|
-
|
|
54
|
+
The resolver passes Pothos's selection plan to Drizzle, loading the requested posts with their
|
|
55
|
+
author. Adding GraphQL fields does not require rewriting this root resolver.
|
|
15
56
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
57
|
+
[Relations](https://pothos-graphql.dev/docs/plugins/drizzle/relations) covers filtering and counts; [Selections](https://pothos-graphql.dev/docs/plugins/drizzle/selections) explains computed
|
|
58
|
+
fields and profiles; [Connections](https://pothos-graphql.dev/docs/plugins/drizzle/connections) handles pagination. [Variants](https://pothos-graphql.dev/docs/plugins/drizzle/variants) and
|
|
59
|
+
[Interfaces](https://pothos-graphql.dev/docs/plugins/drizzle/interfaces) keep private account data separate from public authors. For more
|
|
60
|
+
specialized schemas, [Connection helpers](https://pothos-graphql.dev/docs/plugins/drizzle/connection-helpers) and [Async selections](https://pothos-graphql.dev/docs/plugins/drizzle/async-selections)
|
|
61
|
+
cover custom pagination and asynchronous planning.
|
|
62
|
+
|
|
63
|
+
## Setup
|
|
64
|
+
|
|
65
|
+
Pass your existing Drizzle client and relations to the plugin. These guides assume familiarity
|
|
66
|
+
with Drizzle; use its [driver setup](https://orm.drizzle.team/docs/connect-overview) and
|
|
67
|
+
[relations documentation](https://orm.drizzle.team/docs/relations-v2) to configure the database.
|
|
23
68
|
|
|
24
|
-
|
|
69
|
+
### Installing
|
|
25
70
|
|
|
26
|
-
|
|
71
|
+
```package-install
|
|
72
|
+
npm install --save @pothos/plugin-drizzle
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The plugin requires Drizzle's relational query builder v2. See the
|
|
76
|
+
[version requirements](https://pothos-graphql.dev/docs/plugins/drizzle) before installing or upgrading Drizzle.
|
|
77
|
+
|
|
78
|
+
### Configure the builder
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// builder.ts
|
|
27
82
|
|
|
28
83
|
export interface PothosTypes {
|
|
29
|
-
DrizzleRelations:
|
|
84
|
+
DrizzleRelations: typeof relations;
|
|
85
|
+
Context: { userId: number };
|
|
30
86
|
}
|
|
31
87
|
|
|
32
|
-
const builder = new SchemaBuilder<PothosTypes>({
|
|
88
|
+
export const builder = new SchemaBuilder<PothosTypes>({
|
|
33
89
|
plugins: [DrizzlePlugin],
|
|
34
|
-
drizzle: {
|
|
35
|
-
client: db, // or (ctx) => db if you want to create a request specific client
|
|
36
|
-
getTableConfig,
|
|
37
|
-
relations,
|
|
38
|
-
},
|
|
90
|
+
drizzle: { client: db, getTableConfig, relations },
|
|
39
91
|
});
|
|
40
92
|
```
|
|
41
93
|
|
|
94
|
+
`DrizzleRelations` provides the table and relation types used by Pothos. Import `getTableConfig`
|
|
95
|
+
from the dialect used by your client (`sqlite-core` above, or `pg-core` / `mysql-core`). `client`
|
|
96
|
+
may also be a function `(ctx) => db` for a request-specific client. The examples use
|
|
97
|
+
[request context](https://pothos-graphql.dev/docs/guide/context) to supply the current `userId`.
|
|
98
|
+
|
|
99
|
+
Use this builder to [define GraphQL types and fields](https://pothos-graphql.dev/docs/plugins/drizzle/objects), then call `builder.toSchema()`.
|
|
100
|
+
|
|
42
101
|
### Integration with other plugins
|
|
43
102
|
|
|
44
|
-
|
|
45
|
-
plugins are not required, many examples will assume these plugins have been installed:
|
|
103
|
+
Install and register `@pothos/plugin-relay` to use `drizzleNode` and connection fields:
|
|
46
104
|
|
|
47
105
|
```ts
|
|
48
|
-
import { drizzle } from 'drizzle-orm/...';
|
|
49
|
-
import SchemaBuilder from '@pothos/core';
|
|
50
|
-
import DrizzlePlugin from '@pothos/plugin-drizzle';
|
|
51
|
-
import RelayPlugin from '@pothos/plugin-relay';
|
|
52
|
-
import WithInputPlugin from '@pothos/plugin-with-input';
|
|
53
|
-
import { getTableConfig } from 'drizzle-orm/sqlite-core';
|
|
54
|
-
import { relations } from './db/relations';
|
|
55
106
|
|
|
56
|
-
|
|
107
|
+
// In the builder options:
|
|
108
|
+
plugins: [RelayPlugin, DrizzlePlugin],
|
|
109
|
+
relay: { nodesOnConnection: true },
|
|
110
|
+
```
|
|
57
111
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
112
|
+
`nodesOnConnection` adds the `nodes` convenience field used in these guides. Without it,
|
|
113
|
+
select `edges { node { ... } }` instead.
|
|
61
114
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
115
|
+
The [with-input plugin](https://pothos-graphql.dev/docs/plugins/with-input) is another optional integration. To use
|
|
116
|
+
`t.drizzleFieldWithInput`, install `@pothos/plugin-with-input`, import it, and add it to the
|
|
117
|
+
builder's plugins:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
|
|
121
|
+
// In the builder options:
|
|
122
|
+
plugins: [RelayPlugin, WithInputPlugin, DrizzlePlugin],
|
|
70
123
|
```
|
|
71
124
|
|
|
72
|
-
|
|
125
|
+
### Plugin options
|
|
126
|
+
|
|
127
|
+
- `client`: the drizzle client, or a function returning one from the request context.
|
|
128
|
+
- `getTableConfig`: the `getTableConfig` of your dialect, used to read primary keys and unique
|
|
129
|
+
constraints.
|
|
130
|
+
- `relations`: the relations passed to `drizzle()`.
|
|
131
|
+
- `defaultConnectionSize` / `maxConnectionSize`: the page size a connection uses when the query
|
|
132
|
+
does not ask for one (defaults to 20), and the largest size it will accept (defaults to 100).
|
|
133
|
+
Both can also be set per field with `defaultSize` and `maxSize`.
|
|
134
|
+
- `filterConnectionTotalCount`: see [Connection totalCount](https://pothos-graphql.dev/docs/plugins/drizzle/connections#connection-totalcount).
|
|
135
|
+
- `skipDeferredFragments`: selections inside a `@defer` fragment are left out of the planned query
|
|
136
|
+
by default, and the fragment's fields are loaded through [fallback queries](https://pothos-graphql.dev/docs/plugins/drizzle/relations#fallback-queries)
|
|
137
|
+
when it resolves. Set this to `false` to plan deferred selections with the rest of the query.
|
|
138
|
+
|
|
139
|
+
`skipDeferredFragments` controls query planning; it does not enable incremental execution. The application
|
|
140
|
+
must register the defer directive and use an executor, server transport, and client that support
|
|
141
|
+
the same incremental delivery protocol. With GraphQL.js 17, ordinary `execute` rejects schemas
|
|
142
|
+
containing `@defer` or `@stream`; incremental execution uses `experimentalExecuteIncrementally`.
|
|
143
|
+
For a server integration, see [GraphQL Yoga's defer and stream setup](https://the-guild.dev/graphql/yoga-server/docs/features/defer-stream),
|
|
144
|
+
which uses `@graphql-yoga/plugin-defer-stream`. Check the integration's supported versions when
|
|
145
|
+
choosing an executor and client; adding directives or changing `skipDeferredFragments` alone is
|
|
146
|
+
not sufficient.
|
|
147
|
+
|
|
148
|
+
## Drizzle Objects
|
|
149
|
+
|
|
150
|
+
Use the [configured builder](https://pothos-graphql.dev/docs/plugins/drizzle/setup) to map a Drizzle table to a GraphQL type. Defining a type
|
|
151
|
+
registers its fields; a root lookup makes them queryable.
|
|
152
|
+
|
|
153
|
+
### Defining Objects
|
|
73
154
|
|
|
74
155
|
The `builder.drizzleObject` method can be used to define GraphQL Object types based on a drizzle
|
|
75
156
|
table:
|
|
76
157
|
|
|
77
158
|
```ts
|
|
78
|
-
const
|
|
159
|
+
const User = builder.drizzleObject('users', {
|
|
79
160
|
name: 'User',
|
|
80
161
|
fields: (t) => ({
|
|
81
162
|
firstName: t.exposeString('firstName'),
|
|
@@ -85,723 +166,1278 @@ const UserRef = builder.drizzleObject('users', {
|
|
|
85
166
|
```
|
|
86
167
|
|
|
87
168
|
You will be able to "expose" any column in the table, and GraphQL fields do not need to match the
|
|
88
|
-
names of the columns in your database. The returned `
|
|
169
|
+
names of the columns in your database. The returned `User` can be used like any other `ObjectRef`
|
|
89
170
|
in Pothos.
|
|
90
171
|
|
|
91
|
-
|
|
172
|
+
The example uses `builder.drizzleNode`, which adds a Relay ID and node refetching to the
|
|
173
|
+
same object API. See [Relay](https://pothos-graphql.dev/docs/plugins/drizzle/relay) for node IDs and refetching.
|
|
92
174
|
|
|
93
|
-
|
|
94
|
-
column. To do this, you can define fields with a resolver like any other Pothos object type:
|
|
175
|
+
### Custom fields
|
|
95
176
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
});
|
|
177
|
+
A GraphQL field can combine several columns. The author’s full name declares both columns in
|
|
178
|
+
its selection and computes the string from the loaded row:
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
fullName: t.string({
|
|
182
|
+
select: { columns: { firstName: true, lastName: true } },
|
|
183
|
+
resolve: (user) => `${user.firstName} ${user.lastName}`,
|
|
184
|
+
}),
|
|
105
185
|
```
|
|
106
186
|
|
|
107
|
-
|
|
187
|
+
This field belongs in User's `fields` callback. With the default type selection, all scalar
|
|
188
|
+
columns are available; with `select: {}`, computed fields must declare what they read.
|
|
189
|
+
[Selections](https://pothos-graphql.dev/docs/plugins/drizzle/selections) explains when those columns and relations are loaded.
|
|
108
190
|
|
|
109
|
-
|
|
110
|
-
tables with many columns, it can be more efficient to only select the needed columns. You can
|
|
111
|
-
configure the selected columns, and relations by passing a `select` option when defining the type:
|
|
191
|
+
### Drizzle Fields
|
|
112
192
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
lowercaseName: (users, sql) => sql<string>`lower(${users.firstName})`
|
|
126
|
-
},
|
|
127
|
-
},
|
|
193
|
+
`t.drizzleField` can return a Drizzle object from Query or from any other object type. Its
|
|
194
|
+
resolver receives a `query` function before the usual resolver arguments. Call that function and
|
|
195
|
+
pass its result to `findFirst` or `findMany`: it merges your filters and ordering with the
|
|
196
|
+
requirements of the GraphQL selection.
|
|
197
|
+
|
|
198
|
+
#### A nullable author lookup
|
|
199
|
+
|
|
200
|
+
The author page accepts an integer ID. A missing author returns null, matching the result of
|
|
201
|
+
`findFirst` and the field's `nullable: true` option:
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
builder.queryType({
|
|
128
205
|
fields: (t) => ({
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
resolve: (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
206
|
+
author: t.drizzleField({
|
|
207
|
+
type: 'users',
|
|
208
|
+
nullable: true,
|
|
209
|
+
args: { id: t.arg.int({ required: true }) },
|
|
210
|
+
resolve: (query, _root, args) => {
|
|
211
|
+
return db.query.users.findFirst(
|
|
212
|
+
query({
|
|
213
|
+
where: { id: args.id },
|
|
214
|
+
}),
|
|
215
|
+
);
|
|
216
|
+
},
|
|
137
217
|
}),
|
|
138
218
|
}),
|
|
139
219
|
});
|
|
140
220
|
```
|
|
141
221
|
|
|
142
|
-
|
|
143
|
-
|
|
222
|
+
`author(id: 1) { fullName }` returns Maya Chen. `author(id: 999) { fullName }` returns null.
|
|
223
|
+
Adding `posts { title }` loads the public posts through the same root resolver.
|
|
224
|
+
|
|
225
|
+
For a list field, use `type: ['users']` and `db.query.users.findMany(query())`. A plain lookup
|
|
226
|
+
without arguments can pass no options to `query`; fields with filters pass those options to it.
|
|
144
227
|
|
|
145
|
-
|
|
228
|
+
#### `drizzleFieldWithInput`
|
|
146
229
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
230
|
+
With the [with-input plugin](https://pothos-graphql.dev/docs/plugins/with-input),
|
|
231
|
+
`t.drizzleFieldWithInput` combines `t.drizzleField` with `t.fieldWithInput`. The `input` fields
|
|
232
|
+
become an input object argument, and the resolver still receives the `query` function as its first
|
|
233
|
+
argument. This alternative adds an `authorWithInput` lookup to the same public User type.
|
|
234
|
+
Install and register WithInputPlugin as described in [Setup](https://pothos-graphql.dev/docs/plugins/drizzle/setup#integration-with-other-plugins):
|
|
150
235
|
|
|
151
236
|
```ts
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
resolve: (user) => user.profile.bio,
|
|
167
|
-
}),
|
|
168
|
-
email: t.string({
|
|
169
|
-
select: {
|
|
170
|
-
extras: {
|
|
171
|
-
lowercaseName: (users, sql) => sql<string>`lower(${users.firstName})`
|
|
172
|
-
},
|
|
173
|
-
},
|
|
174
|
-
resolve: (user) => `${user.lowercaseName}@example.com`,
|
|
175
|
-
}),
|
|
237
|
+
builder.queryFields((t) => ({
|
|
238
|
+
authorWithInput: t.drizzleFieldWithInput({
|
|
239
|
+
type: 'users',
|
|
240
|
+
nullable: true,
|
|
241
|
+
input: {
|
|
242
|
+
id: t.input.int({ required: true }),
|
|
243
|
+
},
|
|
244
|
+
resolve: (query, root, args, ctx) => {
|
|
245
|
+
return db.query.users.findFirst(
|
|
246
|
+
query({
|
|
247
|
+
where: { id: args.input.id },
|
|
248
|
+
}),
|
|
249
|
+
);
|
|
250
|
+
},
|
|
176
251
|
}),
|
|
177
|
-
});
|
|
252
|
+
}));
|
|
178
253
|
```
|
|
179
254
|
|
|
180
255
|
## Relations
|
|
181
256
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
relations:
|
|
257
|
+
`t.relation` turns a Drizzle relation into a GraphQL field with its target type and cardinality.
|
|
258
|
+
Register the target GraphQL type as well as the relation.
|
|
185
259
|
|
|
186
|
-
|
|
187
|
-
builder.drizzleObject('profiles', {
|
|
188
|
-
name: 'Profile',
|
|
189
|
-
fields: (t) => ({
|
|
190
|
-
bio: t.exposeString('bio'),
|
|
191
|
-
}),
|
|
192
|
-
});
|
|
260
|
+
### Published posts and profiles
|
|
193
261
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
fields: (t) => ({
|
|
197
|
-
title: t.exposeString('title'),
|
|
198
|
-
author: t.relation('author'),
|
|
199
|
-
}),
|
|
200
|
-
});
|
|
262
|
+
The public author page returns published posts. Drafts belong on the [private Viewer](https://pothos-graphql.dev/docs/plugins/drizzle/variants),
|
|
263
|
+
so the filter belongs on the relation wherever User can be reached:
|
|
201
264
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
265
|
+
```typescript
|
|
266
|
+
posts: t.relation('posts', {
|
|
267
|
+
args: { oldestFirst: t.arg.boolean() },
|
|
268
|
+
query: (args) => ({
|
|
269
|
+
where: { published: true },
|
|
270
|
+
orderBy: {
|
|
271
|
+
createdAt: args.oldestFirst ? 'asc' : 'desc',
|
|
272
|
+
id: args.oldestFirst ? 'asc' : 'desc',
|
|
273
|
+
},
|
|
208
274
|
}),
|
|
209
|
-
})
|
|
275
|
+
}),
|
|
210
276
|
```
|
|
211
277
|
|
|
212
|
-
The
|
|
213
|
-
|
|
278
|
+
The filter applies whenever this User type is queried. An author with no matching posts returns
|
|
279
|
+
an empty list. The `oldestFirst` argument selects the ordering.
|
|
280
|
+
|
|
281
|
+
To return the Profile object, register its GraphQL type and use `t.relation('profile')`. To
|
|
282
|
+
return only its nullable biography, use a [field selection](https://pothos-graphql.dev/docs/plugins/drizzle/selections#only-load-a-profile-when-requested).
|
|
214
283
|
|
|
215
|
-
|
|
284
|
+
### Relation queries
|
|
216
285
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
286
|
+
`query` accepts a static object or a callback that converts field arguments into Drizzle query
|
|
287
|
+
options. Alongside filters and ordering, a relation may use `limit` and `offset`. For example,
|
|
288
|
+
this alternative adds offset pagination to the public posts field:
|
|
220
289
|
|
|
221
290
|
```ts
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
offset: t.arg.int(),
|
|
230
|
-
},
|
|
231
|
-
// query callback receives (args, ctx, pathInfo)
|
|
232
|
-
query: (args) => ({
|
|
233
|
-
limit: args.limit ?? 10,
|
|
234
|
-
offset: args.offset ?? 0,
|
|
235
|
-
where: {
|
|
236
|
-
published: true,
|
|
237
|
-
},
|
|
238
|
-
orderBy: {
|
|
239
|
-
updatedAt: 'desc',
|
|
240
|
-
},
|
|
241
|
-
}),
|
|
242
|
-
}),
|
|
243
|
-
drafts: t.relation('posts', {
|
|
244
|
-
query: {
|
|
245
|
-
where: {
|
|
246
|
-
published: false,
|
|
247
|
-
},
|
|
248
|
-
},
|
|
249
|
-
}),
|
|
291
|
+
posts: t.relation('posts', {
|
|
292
|
+
args: { limit: t.arg.int(), offset: t.arg.int() },
|
|
293
|
+
query: (args) => ({
|
|
294
|
+
limit: args.limit ?? 10,
|
|
295
|
+
offset: args.offset ?? 0,
|
|
296
|
+
where: { published: true },
|
|
297
|
+
orderBy: { createdAt: 'desc', id: 'desc' },
|
|
250
298
|
}),
|
|
251
|
-
})
|
|
299
|
+
}),
|
|
252
300
|
```
|
|
253
301
|
|
|
254
302
|
The query API enables you to define args and convert them into parameters that will be passed into
|
|
255
303
|
the relational query builder. The `query` callback receives `(args, ctx, pathInfo)` where `pathInfo`
|
|
256
|
-
|
|
257
|
-
|
|
304
|
+
describes where in the GraphQL query the relation is being loaded:
|
|
305
|
+
|
|
306
|
+
- `path`: a list of `ParentType.fieldName` strings, from the root field down to the field being
|
|
307
|
+
resolved (eg. `['Query.user', 'User.posts']`).
|
|
308
|
+
- `segments`: one object per entry in `path`, with `field` (the field name), `alias` (the alias used
|
|
309
|
+
in the query, or the field name if none), `parentType` (the name of the type the field is defined
|
|
310
|
+
on), and `isList` (whether the field returns a list).
|
|
311
|
+
|
|
312
|
+
You can read more about the relation query builder api
|
|
313
|
+
[here](https://orm.drizzle.team/docs/rqb-v2)
|
|
314
|
+
|
|
315
|
+
### Fallback queries
|
|
258
316
|
|
|
259
|
-
|
|
317
|
+
A field whose data is not on the row it resolves from is loaded with a fallback query. This happens
|
|
318
|
+
when:
|
|
260
319
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
320
|
+
- The parent row was not loaded through a `t.drizzleField`, `t.relation`, or connection. This
|
|
321
|
+
covers rows a resolver queried itself, and rows that came from somewhere else entirely.
|
|
322
|
+
- A `drizzleField` resolver did not pass the result of `query()` to drizzle.
|
|
323
|
+
- A relation's arguments conflict with a sibling selection of the same relation that was planned
|
|
324
|
+
first.
|
|
325
|
+
|
|
326
|
+
Fallback queries are batched. Every row of a table that needs the same selection in the same tick
|
|
327
|
+
is loaded with one `findMany` filtered on the primary key, or on the first unique column for a
|
|
328
|
+
table that has no primary key, and the rows are matched back to their parents. If the query does
|
|
329
|
+
not return a row for a parent, because it was deleted since it was loaded, or never came from the
|
|
330
|
+
table, that field rejects with `Model users(1) not found`, where the value in parentheses is the
|
|
331
|
+
key that was looked up.
|
|
332
|
+
|
|
333
|
+
The [alias query](https://pothos-graphql.dev/docs/plugins/drizzle/query-planning#compare-two-orderings-of-one-relation) demonstrates
|
|
334
|
+
this fallback: newest and oldest cannot share one loaded posts list.
|
|
335
|
+
|
|
336
|
+
### Related count
|
|
337
|
+
|
|
338
|
+
An author’s count must use the same publication filter as the list. Otherwise the number can
|
|
339
|
+
reveal that unpublished posts exist:
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
postCount: t.relatedCount('posts', { where: eq(posts.published, true) }),
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
`t.relatedCount` returns the number of related rows without loading them. Without a `where`,
|
|
346
|
+
it counts all rows in the relation.
|
|
347
|
+
|
|
348
|
+
The `where` option accepts either a static SQL filter or a function that receives the field arguments
|
|
349
|
+
and context:
|
|
266
350
|
|
|
267
351
|
```ts
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}),
|
|
284
|
-
posts: t.drizzleField({
|
|
285
|
-
type: ['posts'],
|
|
286
|
-
resolve: (query, root, args, ctx) => db.query.posts.findMany(query()),
|
|
287
|
-
}),
|
|
288
|
-
}),
|
|
352
|
+
import { and, eq } from 'drizzle-orm';
|
|
353
|
+
import { posts } from './tables';
|
|
354
|
+
|
|
355
|
+
// In the User fields callback:
|
|
356
|
+
publishedPostsCount: t.relatedCount('posts', {
|
|
357
|
+
args: {
|
|
358
|
+
title: t.arg.string(),
|
|
359
|
+
},
|
|
360
|
+
where: (args, ctx) => {
|
|
361
|
+
if (args.title) {
|
|
362
|
+
return and(eq(posts.published, true), eq(posts.title, args.title));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return eq(posts.published, true);
|
|
366
|
+
},
|
|
289
367
|
});
|
|
290
368
|
```
|
|
291
369
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
370
|
+
For a many-to-many relation (one defined with `.through(...)`), `t.relatedCount` counts distinct
|
|
371
|
+
related rows, so a row reachable through two junction rows counts once. A `t.relatedConnection`'s
|
|
372
|
+
`totalCount` counts the rows the connection pages over instead, which is one per junction row,
|
|
373
|
+
since that is what the relational query builder returns for the relation.
|
|
296
374
|
|
|
297
|
-
## Variants
|
|
298
375
|
|
|
299
|
-
|
|
300
|
-
done using a feature called `variants`. The `variants` API consists of 3 parts:
|
|
376
|
+
### Many-to-many relations
|
|
301
377
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
- A `t.field` method that works similar to `t.relation`, but is used to define a GraphQL field that
|
|
305
|
-
references a variant of the same record.
|
|
378
|
+
A post exposes attached Media objects even though the database stores an attachment row. The
|
|
379
|
+
`through` relation handles that join; Pothos exposes its target like any other relation:
|
|
306
380
|
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
variant: 'Viewer',
|
|
381
|
+
```typescript
|
|
382
|
+
builder.drizzleObject('posts', {
|
|
383
|
+
name: 'Post',
|
|
311
384
|
select: {},
|
|
312
385
|
fields: (t) => ({
|
|
313
386
|
id: t.exposeID('id'),
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
query: {
|
|
319
|
-
|
|
320
|
-
published: false,
|
|
321
|
-
},
|
|
322
|
-
orderBy: {
|
|
323
|
-
updatedAt: 'desc',
|
|
324
|
-
},
|
|
325
|
-
},
|
|
326
|
-
}),
|
|
327
|
-
}),
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
builder.queryType({
|
|
331
|
-
fields: (t) => ({
|
|
332
|
-
me: t.drizzleField({
|
|
333
|
-
// We can use the ref returned by builder.drizzleObject to define our `drizzleField`
|
|
334
|
-
type: Viewer,
|
|
335
|
-
resolve: (query, root, args, ctx) =>
|
|
336
|
-
db.query.users.findFirst(
|
|
337
|
-
query({
|
|
338
|
-
where: {
|
|
339
|
-
id: ctx.user.id,
|
|
340
|
-
},
|
|
341
|
-
}),
|
|
342
|
-
),
|
|
387
|
+
title: t.exposeString('title'),
|
|
388
|
+
author: t.relation('author'),
|
|
389
|
+
media: t.relation('media'),
|
|
390
|
+
mediaConnection: t.relatedConnection('media', {
|
|
391
|
+
query: { orderBy: { id: 'asc' } },
|
|
392
|
+
totalCount: true,
|
|
343
393
|
}),
|
|
344
394
|
}),
|
|
345
395
|
});
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
fields: (t) => ({
|
|
350
|
-
firstName: t.exposeString('firstName'),
|
|
351
|
-
// This field will resolve to the Viewer type, but be set to null if the user is not the current user
|
|
352
|
-
viewer: t.variant(Viewer, {
|
|
353
|
-
isNull: (user, args, ctx) => user.id !== ctx.user?.id,
|
|
354
|
-
}),
|
|
355
|
-
}),
|
|
396
|
+
export const Media = builder.drizzleObject('media', {
|
|
397
|
+
name: 'Media',
|
|
398
|
+
fields: (t) => ({ url: t.exposeString('url'), uploadedBy: t.relation('uploadedBy') }),
|
|
356
399
|
});
|
|
357
400
|
```
|
|
358
401
|
|
|
402
|
+
`t.relation` and `t.relatedConnection` expose the Media type directly; no GraphQL type for the
|
|
403
|
+
junction table is needed.
|
|
359
404
|
|
|
360
|
-
|
|
405
|
+
### Related field
|
|
361
406
|
|
|
362
407
|
The `t.relatedField` method allows you to define a field based on a relation that uses custom
|
|
363
408
|
selections, including aggregations like counts. This is useful when you want to expose derived data
|
|
364
409
|
from a relation without loading the full related records.
|
|
365
410
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
One common use case is adding a count field that efficiently counts related records:
|
|
411
|
+
For a simple count, prefer `t.relatedCount`. Its equivalent using `t.relatedField` illustrates
|
|
412
|
+
how `buildFilter` restricts an expression to the parent's related rows:
|
|
369
413
|
|
|
370
414
|
```ts
|
|
371
|
-
import {
|
|
415
|
+
import { and, eq } from 'drizzle-orm';
|
|
416
|
+
import { posts } from './tables';
|
|
372
417
|
|
|
373
418
|
builder.drizzleNode('users', {
|
|
374
419
|
name: 'User',
|
|
375
420
|
id: { column: (user) => user.id },
|
|
376
421
|
fields: (t) => ({
|
|
377
422
|
firstName: t.exposeString('firstName'),
|
|
378
|
-
//
|
|
423
|
+
// Count only this author’s published posts
|
|
379
424
|
postsCount: t.relatedField('posts', {
|
|
380
425
|
type: 'Int',
|
|
381
426
|
// buildFilter creates the correct WHERE clause for the relation
|
|
382
|
-
select: (buildFilter) =>
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
427
|
+
select: (buildFilter) => {
|
|
428
|
+
return {
|
|
429
|
+
extras: {
|
|
430
|
+
postsCount: (parent) => {
|
|
431
|
+
return db.$count(
|
|
432
|
+
posts,
|
|
433
|
+
and(buildFilter(parent), eq(posts.published, true)),
|
|
434
|
+
);
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
},
|
|
387
439
|
resolve: (user) => user.postsCount,
|
|
388
440
|
}),
|
|
389
441
|
}),
|
|
390
442
|
});
|
|
391
443
|
```
|
|
392
444
|
|
|
393
|
-
|
|
394
|
-
relation
|
|
395
|
-
|
|
445
|
+
`buildFilter(parent)` includes the relation's join conditions and any filter defined on the
|
|
446
|
+
relation. Combine it with the conditions your field needs, then return the selected value from
|
|
447
|
+
`resolve`. Use this helper for custom expressions that `t.relatedCount` does not cover.
|
|
396
448
|
|
|
397
|
-
|
|
449
|
+
`t.relatedField` also accepts the normal field options (`description`, `deprecationReason`,
|
|
450
|
+
`extensions`, and options added by other plugins like `authScopes`). Its `resolve` may be async,
|
|
451
|
+
and receives the resolve `info` as its fourth argument.
|
|
398
452
|
|
|
399
|
-
|
|
400
|
-
all the boilerplate for you:
|
|
453
|
+
## Selections
|
|
401
454
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
});
|
|
455
|
+
Selections describe the database data a GraphQL field needs. The example User starts with
|
|
456
|
+
`select: {}` so requesting one field does not load unrelated columns or a profile.
|
|
457
|
+
|
|
458
|
+
### Field selections
|
|
459
|
+
|
|
460
|
+
An exposed column, such as `t.exposeString('firstName')`, contributes its own selection. A custom
|
|
461
|
+
resolver must declare the columns it reads:
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
fullName: t.string({
|
|
465
|
+
select: { columns: { firstName: true, lastName: true } },
|
|
466
|
+
resolve: (user) => `${user.firstName} ${user.lastName}`,
|
|
467
|
+
}),
|
|
416
468
|
```
|
|
417
469
|
|
|
418
|
-
|
|
419
|
-
and context:
|
|
470
|
+
### Only load a profile when requested
|
|
420
471
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
472
|
+
A biography is a string in GraphQL, but it comes from the optional profile relation. The field
|
|
473
|
+
selects that relation only when requested:
|
|
474
|
+
|
|
475
|
+
```typescript
|
|
476
|
+
bio: t.string({
|
|
477
|
+
nullable: true,
|
|
478
|
+
select: { with: { profile: true } },
|
|
479
|
+
resolve: (user) => user.profile?.bio,
|
|
429
480
|
}),
|
|
430
481
|
```
|
|
431
482
|
|
|
432
|
-
|
|
483
|
+
A missing profile returns null. A name-only query does not load the profile; adding `bio` adds
|
|
484
|
+
the relation to the database query.
|
|
485
|
+
|
|
486
|
+
### Computed SQL values
|
|
487
|
+
|
|
488
|
+
The same mechanism supports SQL expressions through `extras`. For example, a field can select
|
|
489
|
+
`lowercaseName` only when the GraphQL operation requests it:
|
|
433
490
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
491
|
+
```typescript
|
|
492
|
+
lowercaseName: t.string({
|
|
493
|
+
select: {
|
|
494
|
+
extras: {
|
|
495
|
+
lowercaseName: (users, { sql }) => sql<string>`lower(${users.firstName})`,
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
resolve: (user) => user.lowercaseName,
|
|
499
|
+
}),
|
|
500
|
+
```
|
|
437
501
|
|
|
438
|
-
|
|
502
|
+
### Type selections
|
|
439
503
|
|
|
440
|
-
|
|
441
|
-
|
|
504
|
+
By default, a `drizzleObject` gives its resolvers access to all columns of the table. A type-level
|
|
505
|
+
`select` replaces that default and makes its selected data available to every field resolver.
|
|
506
|
+
This alternative always loads the name, profile, and expression:
|
|
442
507
|
|
|
443
508
|
```ts
|
|
444
|
-
builder.
|
|
509
|
+
const User = builder.drizzleObject('users', {
|
|
445
510
|
name: 'User',
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
511
|
+
select: {
|
|
512
|
+
columns: {
|
|
513
|
+
firstName: true,
|
|
514
|
+
lastName: true,
|
|
515
|
+
},
|
|
516
|
+
with: {
|
|
517
|
+
profile: true,
|
|
518
|
+
},
|
|
519
|
+
extras: {
|
|
520
|
+
lowercaseName: (users, { sql }) => sql<string>`lower(${users.firstName})`
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
fields: (t) => {
|
|
524
|
+
return {
|
|
525
|
+
fullName: t.string({
|
|
526
|
+
resolve: (user, args, ctx, info) => `${user.firstName} ${user.lastName}`,
|
|
527
|
+
}),
|
|
528
|
+
bio: t.string({
|
|
529
|
+
nullable: true,
|
|
530
|
+
resolve: (user) => user.profile?.bio,
|
|
531
|
+
}),
|
|
532
|
+
lowercaseName: t.string({
|
|
533
|
+
resolve: (user) => user.lowercaseName,
|
|
534
|
+
}),
|
|
535
|
+
};
|
|
449
536
|
},
|
|
450
|
-
fields: (t) => ({
|
|
451
|
-
firstName: t.exposeString('firstName'),
|
|
452
|
-
lastName: t.exposeString('lastName'),
|
|
453
|
-
}),
|
|
454
537
|
});
|
|
455
538
|
```
|
|
456
539
|
|
|
457
|
-
|
|
540
|
+
Any selections added to the type will be available to consume in all resolvers. Columns that are not
|
|
541
|
+
selected can still be exposed as before.
|
|
458
542
|
|
|
459
|
-
|
|
543
|
+
Use type selections for data required whenever the type is loaded, and field selections for data
|
|
544
|
+
needed by a particular field. Both are merged into the parent query; they do not make each field
|
|
545
|
+
an independent database query. See [Query planning](https://pothos-graphql.dev/docs/plugins/drizzle/query-planning).
|
|
460
546
|
|
|
461
|
-
|
|
547
|
+
## Connections
|
|
462
548
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
fields: (t) => ({
|
|
467
|
-
posts: t.relatedConnection('posts'),
|
|
468
|
-
}),
|
|
469
|
-
});
|
|
470
|
-
```
|
|
471
|
-
|
|
472
|
-
This will automatically define the `Connection`, and `Edge` types, and their respective fields. To
|
|
473
|
-
customize the Connection and Edge types, options for these types can be passed as additional
|
|
474
|
-
arguments to `t.relatedConnection`, just like `t.connection` from the relay plugin. See the
|
|
475
|
-
[relay plugin docs](https://pothos-graphql.dev/docs/plugins/relay) for more details.
|
|
549
|
+
Connection fields combine cursor pagination with Pothos's selection planning. Define the rows a
|
|
550
|
+
field may return; Pothos adds pagination constraints and selects the columns and relations
|
|
551
|
+
requested under each node. The same field supports forward and backward pagination.
|
|
476
552
|
|
|
477
|
-
|
|
478
|
-
|
|
553
|
+
Use `t.drizzleConnection` when your resolver queries the rows and `t.relatedConnection` for a
|
|
554
|
+
Drizzle relation. Both require the [Relay plugin](https://pothos-graphql.dev/docs/plugins/drizzle/setup#integration-with-other-plugins).
|
|
479
555
|
|
|
480
|
-
|
|
481
|
-
performed in reverse order, which requires inverting the orderBy clause. To do this automatically,
|
|
482
|
-
the `t.relatedConnection` method accepts orderBy as an object like `{ asc: column }` or
|
|
483
|
-
`{ desc: column }` rather than using the `asc(column)` and `desc(column)` helpers from drizzle.
|
|
484
|
-
orderBy can still be returned as either a single column or array when ordering by multiple columns.
|
|
556
|
+
### Page through published posts
|
|
485
557
|
|
|
486
|
-
|
|
487
|
-
|
|
558
|
+
The feed returns published posts ordered by creation time. Its resolver passes the result of
|
|
559
|
+
`query()` to Drizzle, just as a `t.drizzleField` does:
|
|
488
560
|
|
|
489
|
-
```
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
posts
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
561
|
+
```typescript
|
|
562
|
+
posts: t.drizzleConnection({
|
|
563
|
+
type: 'posts',
|
|
564
|
+
totalCount: () => {
|
|
565
|
+
return db.$count(posts, eq(posts.published, true));
|
|
566
|
+
},
|
|
567
|
+
resolve: (query) => {
|
|
568
|
+
return db.query.posts.findMany(
|
|
569
|
+
query({
|
|
570
|
+
where: { published: true },
|
|
571
|
+
// Three posts share this timestamp. Pothos adds the primary key
|
|
572
|
+
// to the cursor ordering, so traversing pages still visits each once.
|
|
573
|
+
orderBy: { createdAt: 'desc' },
|
|
501
574
|
}),
|
|
502
|
-
|
|
503
|
-
}
|
|
504
|
-
})
|
|
575
|
+
);
|
|
576
|
+
},
|
|
577
|
+
}),
|
|
505
578
|
```
|
|
506
579
|
|
|
507
|
-
|
|
580
|
+
Pothos uses the selected node fields to plan the database query, including the nested author.
|
|
581
|
+
It also adds the ordering columns needed for cursors, even when the client does not select them.
|
|
582
|
+
When creation times tie, Pothos appends a primary-key tie breaker so posts have distinct positions.
|
|
583
|
+
See [Ordering and cursors](https://pothos-graphql.dev/docs/plugins/drizzle/ordering-and-cursors) for other orderings and column requirements.
|
|
508
584
|
|
|
509
|
-
|
|
585
|
+
A client requests the first page with `first`:
|
|
510
586
|
|
|
511
|
-
```
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
}
|
|
587
|
+
```graphql
|
|
588
|
+
query PostFeed($after: String) {
|
|
589
|
+
posts(first: 2, after: $after) {
|
|
590
|
+
totalCount
|
|
591
|
+
edges {
|
|
592
|
+
cursor
|
|
593
|
+
node {
|
|
594
|
+
title
|
|
595
|
+
author {
|
|
596
|
+
fullName
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
pageInfo {
|
|
601
|
+
endCursor
|
|
602
|
+
hasNextPage
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
528
606
|
```
|
|
529
607
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
as a subquery in the main database query for efficiency.
|
|
608
|
+
For the next page, pass the previous `endCursor` as `after`. To paginate backward, use `last`
|
|
609
|
+
and `before`, taking `before` from a page's `startCursor`:
|
|
533
610
|
|
|
534
611
|
```graphql
|
|
535
|
-
query {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
}
|
|
544
|
-
}
|
|
612
|
+
query PreviousPosts($before: String) {
|
|
613
|
+
posts(last: 2, before: $before) {
|
|
614
|
+
nodes {
|
|
615
|
+
title
|
|
616
|
+
}
|
|
617
|
+
pageInfo {
|
|
618
|
+
startCursor
|
|
619
|
+
hasPreviousPage
|
|
545
620
|
}
|
|
546
621
|
}
|
|
547
622
|
}
|
|
548
623
|
```
|
|
549
624
|
|
|
550
|
-
|
|
625
|
+
Backward pagination retains the connection's declared ordering; it selects the preceding rows
|
|
626
|
+
rather than reversing the response. `nodes` is a convenience field enabled by
|
|
627
|
+
`relay: { nodesOnConnection: true }`. With the default Relay configuration, use `edges { node }`.
|
|
628
|
+
|
|
629
|
+
### Related connections
|
|
630
|
+
|
|
631
|
+
An author's `postsConnection` applies the same publication filter as the feed. `t.relatedConnection`
|
|
632
|
+
plans that relation from the parent selection without a custom resolver:
|
|
633
|
+
|
|
634
|
+
```typescript
|
|
635
|
+
postsConnection: t.relatedConnection('posts', {
|
|
636
|
+
query: { where: { published: true }, orderBy: { createdAt: 'desc' } },
|
|
637
|
+
totalCount: true,
|
|
638
|
+
}),
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
The `query` option accepts an object or a callback receiving field arguments and context, as it
|
|
642
|
+
does on `t.relation`. Connection `orderBy` accepts column names with `'asc'` or `'desc'`, or a
|
|
643
|
+
column or array of columns for ascending order. Pothos can invert these orderings when fetching
|
|
644
|
+
a backward page. The default ordering uses the table's primary key.
|
|
645
|
+
|
|
646
|
+
Both connection methods create Connection and Edge types. Pass additional options to customize
|
|
647
|
+
those types, as with the [Relay plugin's connection fields](https://pothos-graphql.dev/docs/plugins/relay).
|
|
648
|
+
|
|
649
|
+
### Connection totalCount
|
|
650
|
+
|
|
651
|
+
`totalCount` describes the matching rows across all pages. The author's `totalCount: true`
|
|
652
|
+
counts both published posts even when `first: 1` returns only one. Pothos loads the count only
|
|
653
|
+
when it is selected; a query for only `totalCount` does not load the related posts.
|
|
654
|
+
|
|
655
|
+
For `t.relatedConnection`, Pothos derives the count from the relation and its filter. For a root
|
|
656
|
+
`t.drizzleConnection`, supply a `totalCount` resolver, as the feed does above. Its filter must
|
|
657
|
+
match the rows returned by the connection. This resolver receives the usual
|
|
658
|
+
`parent`, `args`, `context`, and `info` arguments. A count-only root query skips the main resolver.
|
|
659
|
+
|
|
660
|
+
By default, a related count includes the `where` returned by the connection's `query`. Setting
|
|
661
|
+
`filterConnectionTotalCount: false` in the builder's `drizzle` options ignores that field filter
|
|
662
|
+
for counts. A filter on the Drizzle relation itself still applies. Keep the default for the
|
|
663
|
+
public author field: counting drafts would reveal data excluded by its publication filter.
|
|
551
664
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
665
|
+
For a many-to-many relation defined with `through`, the count matches the rows being paginated.
|
|
666
|
+
If two junction rows reach the same target, that target appears and is counted twice. This differs
|
|
667
|
+
from [`t.relatedCount`](https://pothos-graphql.dev/docs/plugins/drizzle/relations#related-count), which counts distinct related targets.
|
|
668
|
+
|
|
669
|
+
### Connections with custom edges
|
|
670
|
+
|
|
671
|
+
A direct many-to-many relation works with `t.relatedConnection`. When the attachment itself has
|
|
672
|
+
fields, such as a caption, the edge needs data from the junction row while its node represents
|
|
673
|
+
Media. [Connection helpers](https://pothos-graphql.dev/docs/plugins/drizzle/connection-helpers) shows how to combine that mapping with selection
|
|
674
|
+
planning, pagination, and custom edge fields in one connection.
|
|
675
|
+
|
|
676
|
+
## Ordering and cursors
|
|
677
|
+
|
|
678
|
+
Connections page with a cursor that records where in the ordering the previous page ended. This
|
|
679
|
+
applies to `t.relatedConnection`, `t.drizzleConnection`, and `drizzleConnectionHelpers`.
|
|
680
|
+
|
|
681
|
+
### Tied timestamps in a feed
|
|
682
|
+
|
|
683
|
+
The [post connection](https://pothos-graphql.dev/docs/plugins/drizzle/connections#page-through-published-posts) orders by `createdAt`, which
|
|
684
|
+
can be identical for several posts. Pothos adds a primary-key tie breaker so each post has a
|
|
685
|
+
distinct position in the connection.
|
|
686
|
+
|
|
687
|
+
```typescript
|
|
688
|
+
posts: t.drizzleConnection({
|
|
689
|
+
type: 'posts',
|
|
690
|
+
totalCount: () => {
|
|
691
|
+
return db.$count(posts, eq(posts.published, true));
|
|
692
|
+
},
|
|
693
|
+
resolve: (query) => {
|
|
694
|
+
return db.query.posts.findMany(
|
|
695
|
+
query({
|
|
696
|
+
where: { published: true },
|
|
697
|
+
// Three posts share this timestamp. Pothos adds the primary key
|
|
698
|
+
// to the cursor ordering, so traversing pages still visits each once.
|
|
699
|
+
orderBy: { createdAt: 'desc' },
|
|
700
|
+
}),
|
|
701
|
+
);
|
|
702
|
+
},
|
|
703
|
+
}),
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
#### Unique orderings
|
|
707
|
+
|
|
708
|
+
`orderBy: { createdAt: 'desc' }` does not describe a unique ordering. Rows with the same timestamp
|
|
709
|
+
can be returned in a different order on each query, so paging through the connection may return a row
|
|
710
|
+
twice, or skip it.
|
|
711
|
+
|
|
712
|
+
Pothos adds the primary key to the ordering when the columns you provide are not already unique, so
|
|
713
|
+
this:
|
|
555
714
|
|
|
556
715
|
```ts
|
|
557
|
-
|
|
558
|
-
posts: t.drizzleConnection({
|
|
559
|
-
type: 'posts',
|
|
560
|
-
resolve: (query, root, args, ctx) =>
|
|
561
|
-
db.query.posts.findMany(
|
|
562
|
-
query({
|
|
563
|
-
where: {
|
|
564
|
-
published: true,
|
|
565
|
-
},
|
|
566
|
-
orderBy: {
|
|
567
|
-
id: 'desc',
|
|
568
|
-
},
|
|
569
|
-
}),
|
|
570
|
-
),
|
|
571
|
-
}),
|
|
572
|
-
}));
|
|
716
|
+
orderBy: { createdAt: 'desc' }
|
|
573
717
|
```
|
|
574
718
|
|
|
575
|
-
|
|
719
|
+
is queried as `createdAt desc, id desc`. You can add the tie breaker yourself if you want it in a
|
|
720
|
+
different position or direction:
|
|
721
|
+
|
|
722
|
+
```ts
|
|
723
|
+
orderBy: { createdAt: 'desc', id: 'asc' }
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
Orderings that already contain the primary key, or a unique column or constraint whose columns are
|
|
727
|
+
all marked `notNull()`, are used as provided. Nullable unique columns are not treated as unique,
|
|
728
|
+
because rows containing a `null` are still tied with each other. Uniqueness declared with
|
|
729
|
+
`uniqueIndex()` is not detected, so those orderings still get the primary key appended.
|
|
730
|
+
|
|
731
|
+
Pothos also recognizes composite primary keys declared with `primaryKey({ columns: [...] })`.
|
|
732
|
+
Primary-key columns are treated as non-null for ordering; ensure your database enforces that
|
|
733
|
+
constraint.
|
|
576
734
|
|
|
577
|
-
|
|
735
|
+
Nothing is appended when Pothos cannot find a key it can rely on, which means tables with no primary
|
|
736
|
+
key, and tables whose only unique column is nullable. Those connections keep the ordering you wrote,
|
|
737
|
+
so give them a tie breaker yourself.
|
|
738
|
+
|
|
739
|
+
#### Nullable ordering columns
|
|
740
|
+
|
|
741
|
+
A row whose ordering value is `null` cannot be paged past. `null` is not greater or less than
|
|
742
|
+
anything in SQL, so no row compares as coming after it and the next page comes back empty. The same
|
|
743
|
+
applies to an ordering expression that can evaluate to `null`.
|
|
744
|
+
|
|
745
|
+
Order by columns that are `notNull()`, or give the expression a real value to fall back to with
|
|
746
|
+
`coalesce`.
|
|
747
|
+
|
|
748
|
+
#### Cursor values and precision
|
|
749
|
+
|
|
750
|
+
A cursor stores the values your driver returned to JavaScript, and those values are compared against
|
|
751
|
+
the column when the next page is requested. If the value in the cursor is not the value the database
|
|
752
|
+
has, the comparison will not match the rows you expect.
|
|
753
|
+
|
|
754
|
+
This comes up with timestamps. `timestamp({ mode: 'date' })` returns a JavaScript `Date`, which only
|
|
755
|
+
holds milliseconds, while a Postgres `timestamptz` column stores microseconds. A row stored at
|
|
756
|
+
`.086068` produces a cursor containing `.086`, and `created_at < '.086'` does not match the other
|
|
757
|
+
rows in that millisecond, so they are never returned on any page.
|
|
758
|
+
|
|
759
|
+
There are two ways to avoid this.
|
|
760
|
+
|
|
761
|
+
The first is to map the column so the full value reaches JavaScript. `mode: 'string'` returns the
|
|
762
|
+
timestamp as text, with the microseconds intact:
|
|
763
|
+
|
|
764
|
+
```ts
|
|
765
|
+
export const posts = pgTable('posts', {
|
|
766
|
+
id: serial('id').primaryKey(),
|
|
767
|
+
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).notNull(),
|
|
768
|
+
});
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
The cursor stores that text, and Postgres parses it back to the value it stored. The column is still
|
|
772
|
+
what gets ordered and compared, so an index on it is still used. The field is typed as a `string`
|
|
773
|
+
rather than a `Date`.
|
|
774
|
+
|
|
775
|
+
The second is to order by an expression that returns the full value, which lets you keep the `Date`
|
|
776
|
+
mapping:
|
|
578
777
|
|
|
579
778
|
```ts
|
|
580
779
|
builder.queryFields((t) => ({
|
|
581
780
|
posts: t.drizzleConnection({
|
|
582
781
|
type: 'posts',
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
resolve: (query, root, args, ctx) =>
|
|
586
|
-
db.query.posts.findMany(
|
|
782
|
+
resolve: (query) => {
|
|
783
|
+
return db.query.posts.findMany(
|
|
587
784
|
query({
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
orderBy: {
|
|
592
|
-
id: 'desc',
|
|
785
|
+
extras: {
|
|
786
|
+
createdAtExact: (table) =>
|
|
787
|
+
sql`to_char(${table.createdAt} AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')`,
|
|
593
788
|
},
|
|
789
|
+
orderBy: { createdAtExact: 'desc' },
|
|
594
790
|
}),
|
|
595
|
-
)
|
|
791
|
+
);
|
|
792
|
+
},
|
|
596
793
|
}),
|
|
597
794
|
}));
|
|
598
795
|
```
|
|
599
796
|
|
|
600
|
-
|
|
797
|
+
#### Ordering by an expression
|
|
798
|
+
|
|
799
|
+
`orderBy` accepts the name of any extra declared in the same query. Pothos orders by the expression,
|
|
800
|
+
builds the cursor from the value it returns, and compares the expression when paging:
|
|
801
|
+
|
|
802
|
+
```ts
|
|
803
|
+
query({
|
|
804
|
+
extras: { titleLength: (table) => sql`length(${table.title})` },
|
|
805
|
+
orderBy: { titleLength: 'asc' },
|
|
806
|
+
})
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
Extras used this way should be written as a callback. Drizzle aliases the table it queries and passes
|
|
810
|
+
that alias to the callback, so an expression built from the imported table object will reference a
|
|
811
|
+
name the query does not have.
|
|
601
812
|
|
|
602
|
-
|
|
813
|
+
With `drizzleConnectionHelpers`, declare the extra in `query` rather than in `select`. Only `query`
|
|
814
|
+
is read when the ordering is resolved.
|
|
603
815
|
|
|
604
|
-
|
|
816
|
+
The expression also needs to sort in the same order its values compare. A timestamp formatted as
|
|
817
|
+
fixed width UTC text sorts the same way it does chronologically, but a local time or variable width
|
|
818
|
+
format does not, and will skip rows.
|
|
605
819
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
820
|
+
## Relay nodes
|
|
821
|
+
|
|
822
|
+
The Relay plugin supplies node IDs and cursor connections. Register it with Drizzle as shown in
|
|
823
|
+
[Setup](https://pothos-graphql.dev/docs/plugins/drizzle/setup#integration-with-other-plugins); [Connections](https://pothos-graphql.dev/docs/plugins/drizzle/connections) covers pagination.
|
|
824
|
+
|
|
825
|
+
### Define a node
|
|
826
|
+
|
|
827
|
+
`builder.drizzleNode` uses the same fields and selections as `builder.drizzleObject`, and adds
|
|
828
|
+
an ID and a root node lookup. The example User uses its integer primary key:
|
|
829
|
+
|
|
830
|
+
```ts
|
|
831
|
+
builder.drizzleNode('users', {
|
|
832
|
+
name: 'User',
|
|
833
|
+
id: { column: (user) => user.id },
|
|
834
|
+
fields: (t) => ({ firstName: t.exposeString('firstName') }),
|
|
835
|
+
});
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
This minimal alternative shows the node options. The example User adds full name, profile,
|
|
839
|
+
and published posts to the same type. Other ID field options can be passed alongside `column`;
|
|
840
|
+
for a composite primary key, `column` can return a list of columns.
|
|
841
|
+
|
|
842
|
+
### Refetch a public author
|
|
843
|
+
|
|
844
|
+
The ID returned by User can be passed back to
|
|
845
|
+
`node` to fetch the author with a new selection:
|
|
846
|
+
|
|
847
|
+
```graphql
|
|
848
|
+
query RefetchAuthor {
|
|
849
|
+
node(id: "VXNlcjox") {
|
|
850
|
+
... on User {
|
|
851
|
+
fullName
|
|
852
|
+
posts {
|
|
853
|
+
title
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
The operation uses the ID of User 1. The node
|
|
861
|
+
returns the same public fields as the author lookup, including only published posts.
|
|
862
|
+
|
|
863
|
+
### Authorize the node load path
|
|
864
|
+
|
|
865
|
+
> [!WARNING]
|
|
866
|
+
> Defining a node creates a direct lookup through `node` and `nodes`. These lookups bypass custom
|
|
867
|
+
> root resolvers, so permission checks or visibility filters on a list or parent field do not protect
|
|
868
|
+
> node refetches. An encoded global ID is an identifier, not proof of permission.
|
|
869
|
+
>
|
|
870
|
+
> Apply an access policy to node loading, the type, or its fields as appropriate. See
|
|
871
|
+
> [Authorizing Relay nodes](https://pothos-graphql.dev/docs/plugins/scope-auth/relay-nodes) for scope patterns and their limits.
|
|
872
|
+
|
|
873
|
+
The public User type here can be refetched by anyone. Its
|
|
874
|
+
[published-posts relation](https://pothos-graphql.dev/docs/plugins/drizzle/relations#published-posts-and-profiles) applies the publication filter
|
|
875
|
+
wherever User is reached, including through `node`. Post stays an ordinary Drizzle object;
|
|
876
|
+
defining a public feed does not also make individual drafts globally refetchable.
|
|
877
|
+
|
|
878
|
+
## Type variants
|
|
879
|
+
|
|
880
|
+
A variant gives another GraphQL representation to the same database row. The publishing API
|
|
881
|
+
uses public User fields for author pages and a private Viewer for the signed-in account.
|
|
882
|
+
|
|
883
|
+
The API has three parts:
|
|
884
|
+
|
|
885
|
+
- `variant` registers an additional type instead of naming the primary type with `name`.
|
|
886
|
+
- `t.variant` returns another representation of the same row.
|
|
887
|
+
- A relation's `type` option can select a variant using its returned object ref.
|
|
888
|
+
|
|
889
|
+
### A single object variant
|
|
890
|
+
|
|
891
|
+
Use `variant` to define Viewer alongside the primary User type. `t.variant('users')` exposes
|
|
892
|
+
the public representation of the same row:
|
|
893
|
+
|
|
894
|
+
```ts
|
|
895
|
+
const Viewer = builder.drizzleObject('users', {
|
|
896
|
+
variant: 'Viewer',
|
|
897
|
+
select: {},
|
|
898
|
+
fields: (t) => {
|
|
899
|
+
return {
|
|
900
|
+
id: t.exposeID('id'),
|
|
901
|
+
user: t.variant('users'),
|
|
902
|
+
drafts: t.relation('posts', {
|
|
903
|
+
query: {
|
|
904
|
+
where: { published: false },
|
|
905
|
+
orderBy: { createdAt: 'desc', id: 'desc' },
|
|
906
|
+
},
|
|
907
|
+
}),
|
|
908
|
+
};
|
|
909
|
+
},
|
|
910
|
+
});
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
### A viewer with role-specific fields
|
|
914
|
+
|
|
915
|
+
When viewer fields differ by role, Viewer can instead be an interface with object variants
|
|
916
|
+
for each role. The following definition uses this structure; [Interfaces](https://pothos-graphql.dev/docs/plugins/drizzle/interfaces) explains
|
|
917
|
+
its implementations:
|
|
609
918
|
|
|
610
919
|
```typescript
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
920
|
+
export const Viewer = builder.drizzleInterface('users', {
|
|
921
|
+
variant: 'Viewer',
|
|
922
|
+
select: { columns: { id: true, role: true } },
|
|
923
|
+
resolveType: (user) => (user.role === 'editor' ? 'EditorViewer' : 'AuthorViewer'),
|
|
614
924
|
fields: (t) => ({
|
|
615
|
-
|
|
616
|
-
|
|
925
|
+
user: t.variant('users'),
|
|
926
|
+
email: t.exposeString('email'),
|
|
927
|
+
drafts: t.relation('posts', {
|
|
928
|
+
query: { where: { published: false }, orderBy: { id: 'asc' } },
|
|
929
|
+
}),
|
|
617
930
|
}),
|
|
618
931
|
});
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
The root lookup uses the authenticated context ID. It does not accept an arbitrary author's ID:
|
|
935
|
+
|
|
936
|
+
```typescript
|
|
937
|
+
me: t.drizzleField({
|
|
938
|
+
type: Viewer,
|
|
939
|
+
nullable: true,
|
|
940
|
+
resolve: (query, _root, _args, ctx) => {
|
|
941
|
+
return db.query.users.findFirst(
|
|
942
|
+
query({
|
|
943
|
+
where: { id: ctx.userId },
|
|
944
|
+
}),
|
|
945
|
+
);
|
|
946
|
+
},
|
|
947
|
+
}),
|
|
948
|
+
```
|
|
619
949
|
|
|
950
|
+
Maya (`userId: 1`) sees “Planning the spring exchange”; Leo (`userId: 2`) sees “Saving rainwater.”
|
|
951
|
+
The interface and variants describe the shapes; the root lookup restricts ownership. Public
|
|
952
|
+
author fields and post connections return published posts only.
|
|
620
953
|
|
|
954
|
+
### Conditionally expose another variant
|
|
621
955
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
},
|
|
956
|
+
The publishing User also exposes its Viewer conditionally. Define that field after Viewer is registered,
|
|
957
|
+
which avoids a circular reference between their definitions:
|
|
958
|
+
|
|
959
|
+
```typescript
|
|
960
|
+
builder.drizzleObjectField('users', 'viewer', (t) =>
|
|
961
|
+
t.variant(Viewer, {
|
|
962
|
+
select: { columns: { id: true } },
|
|
963
|
+
isNull: (user, _args, ctx) => user.id !== ctx.userId,
|
|
631
964
|
}),
|
|
632
|
-
|
|
633
|
-
|
|
965
|
+
);
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
The ownership check is essential: without it an arbitrary author's public row could reveal that
|
|
969
|
+
author's drafts. `isNull` makes the variant field null when its parent is another account.
|
|
970
|
+
|
|
971
|
+
A variant field accepts a `select` just like other fields. Here it loads `id` for the ownership
|
|
972
|
+
check; the target variant contributes the selections required by its own fields.
|
|
973
|
+
|
|
974
|
+
### Relation variants
|
|
975
|
+
|
|
976
|
+
To use a different representation of a related row, pass its object ref to `t.relation`'s `type`
|
|
977
|
+
option. The variant must represent the relation's target table. For example, this alternative
|
|
978
|
+
adds a compact PostSummary representation and selects it for a public author's posts:
|
|
979
|
+
|
|
980
|
+
```ts
|
|
981
|
+
const PostSummary = builder.drizzleObject('posts', {
|
|
982
|
+
variant: 'PostSummary',
|
|
983
|
+
fields: (t) => ({ title: t.exposeString('title') }),
|
|
634
984
|
});
|
|
635
985
|
|
|
636
|
-
builder.drizzleObjectField('
|
|
637
|
-
t.
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
select: (args, ctx, nestedSelection) => ({
|
|
643
|
-
with: {
|
|
644
|
-
userRoles: rolesConnection.getQuery(args, ctx, nestedSelection),
|
|
645
|
-
},
|
|
646
|
-
}),
|
|
647
|
-
// This helper takes a list of nodes and formats them for the connection
|
|
648
|
-
resolve: (user, args, ctx) => {
|
|
649
|
-
return rolesConnection.resolve(user.userRoles, args, ctx, user);
|
|
986
|
+
builder.drizzleObjectField('users', 'postSummaries', (t) =>
|
|
987
|
+
t.relation('posts', {
|
|
988
|
+
type: PostSummary,
|
|
989
|
+
query: {
|
|
990
|
+
where: { published: true },
|
|
991
|
+
orderBy: { id: 'asc' },
|
|
650
992
|
},
|
|
651
993
|
}),
|
|
652
994
|
);
|
|
653
995
|
```
|
|
654
996
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
997
|
+
Two variants selected for the same row have their type-level selections merged. Conflicting
|
|
998
|
+
relation arguments or extras can fail; see
|
|
999
|
+
[Conflicting selections between variants](https://pothos-graphql.dev/docs/plugins/drizzle/query-planning#conflicting-selections-between-variants).
|
|
658
1000
|
|
|
659
|
-
|
|
660
|
-
connections share the same model, and pagination happens directly on a relation to nodes type (even
|
|
661
|
-
if that relation is nested).
|
|
1001
|
+
## Interfaces
|
|
662
1002
|
|
|
663
|
-
|
|
664
|
-
|
|
1003
|
+
### Interfaces
|
|
1004
|
+
|
|
1005
|
+
`builder.drizzleInterface` works like `builder.drizzleObject`. It can define the primary type
|
|
1006
|
+
or a variant of a table. Here Viewer is a variant, leaving User as the public primary type.
|
|
1007
|
+
|
|
1008
|
+
### Define the interface
|
|
665
1009
|
|
|
666
|
-
|
|
1010
|
+
```typescript
|
|
1011
|
+
export const Viewer = builder.drizzleInterface('users', {
|
|
1012
|
+
variant: 'Viewer',
|
|
1013
|
+
select: { columns: { id: true, role: true } },
|
|
1014
|
+
resolveType: (user) => (user.role === 'editor' ? 'EditorViewer' : 'AuthorViewer'),
|
|
667
1015
|
fields: (t) => ({
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
with: {
|
|
673
|
-
comments: commentConnectionHelpers.getQuery(args, ctx, nestedSelection),
|
|
674
|
-
},
|
|
675
|
-
}),
|
|
676
|
-
resolve: (parent, args, ctx) => commentConnectionHelpers.resolve(parent.comments, args, ctx),
|
|
1016
|
+
user: t.variant('users'),
|
|
1017
|
+
email: t.exposeString('email'),
|
|
1018
|
+
drafts: t.relation('posts', {
|
|
1019
|
+
query: { where: { published: false }, orderBy: { id: 'asc' } },
|
|
677
1020
|
}),
|
|
678
1021
|
}),
|
|
679
1022
|
});
|
|
680
1023
|
```
|
|
681
1024
|
|
|
682
|
-
|
|
1025
|
+
`resolveType` uses the selected `role` discriminator and returns GraphQL type names. Returning
|
|
1026
|
+
names avoids a circular reference between the interface and its implementations.
|
|
1027
|
+
|
|
1028
|
+
### Selecting a viewer implementation
|
|
1029
|
+
|
|
1030
|
+
```typescript
|
|
1031
|
+
builder.drizzleObject('users', {
|
|
1032
|
+
variant: 'EditorViewer',
|
|
1033
|
+
interfaces: [Viewer],
|
|
1034
|
+
select: { columns: { id: true, role: true } },
|
|
1035
|
+
fields: (t) => ({ canReviewSubmissions: t.boolean({ resolve: () => true }) }),
|
|
1036
|
+
});
|
|
1037
|
+
builder.drizzleObject('users', {
|
|
1038
|
+
variant: 'AuthorViewer',
|
|
1039
|
+
interfaces: [Viewer],
|
|
1040
|
+
select: { columns: { id: true, role: true } },
|
|
1041
|
+
});
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
The interface's `select` is planned whenever a field returns the interface. An implementation's
|
|
1045
|
+
own selection is planned when a fragment narrows to it. Selections are not inherited; put data
|
|
1046
|
+
required by an implementation in its selection as well. An implementation may select additional
|
|
1047
|
+
columns or expressions for fields that do not belong on the interface.
|
|
1048
|
+
|
|
1049
|
+
Maya (`userId: 1`) resolves to EditorViewer with `canReviewSubmissions: true`. Leo (`userId: 2`)
|
|
1050
|
+
resolves to AuthorViewer, so the editor fragment contributes no field. The interface describes
|
|
1051
|
+
those result shapes; the authenticated root lookup enforces whose drafts are returned.
|
|
1052
|
+
|
|
1053
|
+
### Extending an interface
|
|
1054
|
+
|
|
1055
|
+
Fields can be added to an interface later with `builder.drizzleInterfaceField` and
|
|
1056
|
+
`builder.drizzleInterfaceFields`, which take the interface ref (or the table name) like their
|
|
1057
|
+
`drizzleObjectField(s)` counterparts:
|
|
683
1058
|
|
|
684
1059
|
```ts
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
1060
|
+
builder.drizzleInterfaceFields(Viewer, (t) => ({
|
|
1061
|
+
publishedPosts: t.relatedConnection('posts', {
|
|
1062
|
+
query: { where: { published: true } },
|
|
1063
|
+
}),
|
|
1064
|
+
}));
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
An object type implementing a drizzle interface must be based on the same table. A plain object
|
|
1068
|
+
type that implements one is planned with the interface's table, so fragments on it will select the
|
|
1069
|
+
relations it inherits.
|
|
1070
|
+
|
|
1071
|
+
## Query planning
|
|
1072
|
+
|
|
1073
|
+
This page describes how the plugin turns a GraphQL query into drizzle queries, which is worth
|
|
1074
|
+
knowing when a schema issues more queries than you expect.
|
|
1075
|
+
|
|
1076
|
+
### How fields get their data
|
|
1077
|
+
|
|
1078
|
+
A field either reads its data from a row that has already been loaded, or runs a query of its own.
|
|
1079
|
+
|
|
1080
|
+
A field's `select` is planned into the query of the nearest ancestor that runs one: a
|
|
1081
|
+
`t.drizzleField`, a `t.relation`, a connection, or a [fallback query](https://pothos-graphql.dev/docs/plugins/drizzle/relations#fallback-queries). The field
|
|
1082
|
+
then reads what it needs off the loaded row, without a query of its own. A field runs its own query
|
|
1083
|
+
when its `resolve` queries drizzle directly, and when the plugin issues a fallback query for a
|
|
1084
|
+
relation that is missing from the row.
|
|
1085
|
+
|
|
1086
|
+
A field can do both. A `t.drizzleField`, or any other field with a `select`, nested under one of
|
|
1087
|
+
those ancestors has its `select` planned into the parent's row, and still runs its own query when
|
|
1088
|
+
it resolves.
|
|
1089
|
+
|
|
1090
|
+
A field-level `select` is merged into the same query as its siblings and the type-level selection,
|
|
1091
|
+
rather than being kept separate for that field. Two selections of the same relation share a place
|
|
1092
|
+
in that query only when their arguments (`where`, `orderBy`, `limit`, ...) match. When they differ,
|
|
1093
|
+
the first one planned wins, and the other is loaded with a query of its own. A type-level `select`
|
|
1094
|
+
is planned before any field's selection, no matter where they appear in the document, and fields
|
|
1095
|
+
are planned in the order they are selected.
|
|
1096
|
+
|
|
1097
|
+
### Compare two orderings of one relation
|
|
1098
|
+
|
|
1099
|
+
A client may need both the newest and oldest published posts on one author page:
|
|
1100
|
+
|
|
1101
|
+
```graphql
|
|
1102
|
+
query CompareOrderings {
|
|
1103
|
+
author(id: 1) {
|
|
1104
|
+
newest: posts {
|
|
1105
|
+
title
|
|
692
1106
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
accepted: true,
|
|
1107
|
+
oldest: posts(oldestFirst: true) {
|
|
1108
|
+
title
|
|
696
1109
|
}
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
```
|
|
1113
|
+
|
|
1114
|
+
The [published-posts field](https://pothos-graphql.dev/docs/plugins/drizzle/relations#published-posts-and-profiles) translates those arguments
|
|
1115
|
+
into different database orderings. The two fields cannot reuse the same loaded
|
|
1116
|
+
relation because their orderings differ. The additional ordering is loaded through a fallback
|
|
1117
|
+
query.
|
|
1118
|
+
|
|
1119
|
+
In this example, selecting one ordering executes one SQL statement; selecting both executes
|
|
1120
|
+
two. The number of GraphQL field resolvers does not determine the number of database queries.
|
|
1121
|
+
|
|
1122
|
+
```typescript
|
|
1123
|
+
posts: t.relation('posts', {
|
|
1124
|
+
args: { oldestFirst: t.arg.boolean() },
|
|
1125
|
+
query: (args) => ({
|
|
1126
|
+
where: { published: true },
|
|
1127
|
+
orderBy: {
|
|
1128
|
+
createdAt: args.oldestFirst ? 'asc' : 'desc',
|
|
1129
|
+
id: args.oldestFirst ? 'asc' : 'desc',
|
|
703
1130
|
},
|
|
704
1131
|
}),
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
});
|
|
1132
|
+
}),
|
|
1133
|
+
```
|
|
708
1134
|
|
|
1135
|
+
### Nested selections
|
|
709
1136
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
1137
|
+
A `select` function receives `nestedSelection`, which plans the selection beneath the field for the
|
|
1138
|
+
field's own type. It returns the query config for that table, or a `many` config for a list field.
|
|
1139
|
+
Any keys you pass in are kept as they were given, so `columns` passed to `nestedSelection` will
|
|
1140
|
+
still narrow the parent shape:
|
|
1141
|
+
|
|
1142
|
+
```ts
|
|
1143
|
+
builder.drizzleObject('users', {
|
|
1144
|
+
name: 'User',
|
|
1145
|
+
fields: (t) => ({
|
|
1146
|
+
previewPosts: t.field({
|
|
1147
|
+
type: ['posts'],
|
|
1148
|
+
select: (args, ctx, nestedSelection) => {
|
|
1149
|
+
return {
|
|
1150
|
+
// what the query selects on the posts, limited to one
|
|
1151
|
+
with: {
|
|
1152
|
+
posts: nestedSelection({
|
|
1153
|
+
where: { published: true },
|
|
1154
|
+
orderBy: { id: 'asc' },
|
|
1155
|
+
limit: 1,
|
|
1156
|
+
}),
|
|
1157
|
+
},
|
|
1158
|
+
};
|
|
718
1159
|
},
|
|
1160
|
+
resolve: (user) => user.posts,
|
|
719
1161
|
}),
|
|
720
|
-
resolve: (user, args, ctx) => rolesConnection.resolve(user.userRoles, args, ctx, user),
|
|
721
1162
|
}),
|
|
722
|
-
);
|
|
1163
|
+
});
|
|
1164
|
+
```
|
|
1165
|
+
|
|
1166
|
+
`nestedSelection(query, path, type)` takes two more arguments:
|
|
1167
|
+
|
|
1168
|
+
- `path`: a list of field names, for selecting a field nested under the field's type. A segment can
|
|
1169
|
+
also be written as `{ name, type }` to pin the implementation the field must be found under.
|
|
1170
|
+
- `type`: a member of an interface or union, to read the selection as that type.
|
|
1171
|
+
|
|
1172
|
+
The `nestedSelection` function also carries the `path` and `segments` of the field being planned,
|
|
1173
|
+
which are described under [Relation queries](https://pothos-graphql.dev/docs/plugins/drizzle/relations#relation-queries).
|
|
1174
|
+
|
|
1175
|
+
### Async selections
|
|
1176
|
+
|
|
1177
|
+
When a selection depends on asynchronous request data, enable `AsyncSelections: true` and await
|
|
1178
|
+
nested plans. [Async selections](https://pothos-graphql.dev/docs/plugins/drizzle/async-selections) covers the builder configuration, supported
|
|
1179
|
+
callbacks, helper options, and errors caused by unsettled promises.
|
|
1180
|
+
|
|
1181
|
+
### Conflicting selections between variants
|
|
1182
|
+
|
|
1183
|
+
When a query selects two variants of one table for the same row, either with a fragment on each
|
|
1184
|
+
under one field, or through a `t.variant` field, the plugin will throw a `PothosValidationError` if
|
|
1185
|
+
their `select` options ask for the same relation with different arguments:
|
|
1186
|
+
|
|
1187
|
+
```
|
|
1188
|
+
PothosValidationError: Type-level selections of Viewer and Admin conflict on relation "posts".
|
|
1189
|
+
Move the relation arguments to a field-level select on one of the types.
|
|
1190
|
+
```
|
|
1191
|
+
|
|
1192
|
+
The same applies to two variants defining the same `extras` key with different functions
|
|
1193
|
+
(`... conflict on extra "lowercaseName"`). Both variants describe one row, so their `select`
|
|
1194
|
+
options are merged into a single query.
|
|
1195
|
+
|
|
1196
|
+
To fix this, move the relation with its arguments, or the extra, into the `select` of the field
|
|
1197
|
+
that needs it on one of the variants. A field-level selection that conflicts with what the row
|
|
1198
|
+
already holds falls back to a query for that field, rather than failing the request.
|
|
1199
|
+
|
|
1200
|
+
## Connection helpers
|
|
1201
|
+
|
|
1202
|
+
An image can appear in several posts, with a different caption in each. The Media row describes
|
|
1203
|
+
the image; the PostMedia row describes its attachment to a post. A connection can expose the
|
|
1204
|
+
image as its node and the attachment's caption as an edge field:
|
|
1205
|
+
|
|
1206
|
+
```graphql
|
|
1207
|
+
query PostAttachments {
|
|
1208
|
+
author(id: 1) {
|
|
1209
|
+
postsConnection(first: 1) {
|
|
1210
|
+
nodes {
|
|
1211
|
+
title
|
|
1212
|
+
attachments(first: 2) {
|
|
1213
|
+
edges {
|
|
1214
|
+
caption
|
|
1215
|
+
node {
|
|
1216
|
+
url
|
|
1217
|
+
uploadedBy {
|
|
1218
|
+
fullName
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
pageInfo {
|
|
1223
|
+
endCursor
|
|
1224
|
+
hasNextPage
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
723
1231
|
```
|
|
724
1232
|
|
|
725
|
-
|
|
1233
|
+
`t.relatedConnection` handles a relation whose rows are the connection's nodes. Use
|
|
1234
|
+
`drizzleConnectionHelpers` with `t.connection` when the rows being paginated differ from those
|
|
1235
|
+
nodes, or when you need to load the rows yourself. The helper supplies the selection, cursor,
|
|
1236
|
+
and result handling that a normal Relay connection field does not have.
|
|
726
1237
|
|
|
727
|
-
|
|
1238
|
+
### Select the nodes through the attachment
|
|
1239
|
+
|
|
1240
|
+
Import `drizzleConnectionHelpers` from `@pothos/plugin-drizzle` and use the builder configured
|
|
1241
|
+
with the Drizzle and Relay plugins. The helper targets `postMedia`. Its `select` uses
|
|
1242
|
+
`nestedSelection()` to request the Media fields
|
|
1243
|
+
selected beneath the connection's nodes; `resolveNode` returns that related Media row:
|
|
728
1244
|
|
|
729
1245
|
```typescript
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
1246
|
+
const attachmentArgs = builder.args((t) => ({
|
|
1247
|
+
hasCaption: t.boolean({ defaultValue: false }),
|
|
1248
|
+
}));
|
|
1249
|
+
|
|
1250
|
+
const attachments = drizzleConnectionHelpers(builder, 'postMedia', {
|
|
1251
|
+
args: () => attachmentArgs,
|
|
1252
|
+
query: (args) => ({
|
|
1253
|
+
orderBy: { id: 'asc' },
|
|
1254
|
+
where: args.hasCaption ? { caption: { isNotNull: true } } : {},
|
|
735
1255
|
}),
|
|
736
|
-
|
|
1256
|
+
select: (nestedSelection) => {
|
|
1257
|
+
return {
|
|
1258
|
+
columns: { caption: true },
|
|
1259
|
+
with: { media: nestedSelection() },
|
|
1260
|
+
};
|
|
1261
|
+
},
|
|
1262
|
+
resolveNode: (attachment) => attachment.media,
|
|
737
1263
|
});
|
|
1264
|
+
```
|
|
1265
|
+
|
|
1266
|
+
The connection orders and creates cursors from attachment IDs, not Media IDs. An image shared
|
|
1267
|
+
by two posts can therefore have a different caption and connection position in each post.
|
|
1268
|
+
The nested uploader selection is still planned from the requested Media fields.
|
|
1269
|
+
|
|
1270
|
+
The helper's `args` and `query` options define a `hasCaption` argument. When it is true, the
|
|
1271
|
+
connection includes only attachments with a caption. Its default, false, includes all attachments.
|
|
1272
|
+
`getArgs()` exposes that argument on the field, and `getQuery()` combines the filter and ordering
|
|
1273
|
+
with the requested page and node selection.
|
|
738
1274
|
|
|
739
|
-
|
|
740
|
-
|
|
1275
|
+
### Add the connection and its edge fields
|
|
1276
|
+
|
|
1277
|
+
`Media` below is the object ref returned by `builder.drizzleObject('media', ...)`.
|
|
1278
|
+
A normal `t.connection` needs an explicit `select` to load the helper's query on the parent.
|
|
1279
|
+
Its resolver passes the selected attachment rows to the helper's `resolve` method:
|
|
1280
|
+
|
|
1281
|
+
```typescript
|
|
1282
|
+
builder.drizzleObjectField('posts', 'attachments', (t) =>
|
|
1283
|
+
t.connection(
|
|
741
1284
|
{
|
|
742
|
-
type:
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
),
|
|
1285
|
+
type: Media,
|
|
1286
|
+
args: attachments.getArgs(),
|
|
1287
|
+
select: (args, ctx, nestedSelection) => {
|
|
1288
|
+
return {
|
|
1289
|
+
with: {
|
|
1290
|
+
attachments: attachments.getQuery(args, ctx, nestedSelection),
|
|
1291
|
+
},
|
|
1292
|
+
};
|
|
1293
|
+
},
|
|
1294
|
+
resolve: (post, args, ctx) => {
|
|
1295
|
+
return attachments.resolve(post.attachments, args, ctx);
|
|
1296
|
+
},
|
|
755
1297
|
},
|
|
756
1298
|
{},
|
|
757
|
-
// options for the edge object
|
|
758
1299
|
{
|
|
759
|
-
// define the additional fields on the edge object
|
|
760
1300
|
fields: (edge) => ({
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
resolve: (role) => role.createdAt,
|
|
1301
|
+
caption: edge.string({
|
|
1302
|
+
nullable: true,
|
|
1303
|
+
resolve: (attachment) => attachment.caption,
|
|
765
1304
|
}),
|
|
766
1305
|
}),
|
|
767
1306
|
},
|
|
768
1307
|
),
|
|
769
|
-
|
|
1308
|
+
);
|
|
770
1309
|
```
|
|
771
1310
|
|
|
772
|
-
|
|
1311
|
+
The third argument to `t.connection` configures the Edge type. Each edge retains the selected
|
|
1312
|
+
attachment fields, so its `caption` resolver reads the attachment's caption, while `node` returns
|
|
1313
|
+
the Media selected by `resolveNode`. A missing caption returns null.
|
|
773
1314
|
|
|
774
|
-
|
|
1315
|
+
The post has three attachments. Its first unfiltered page of two has `hasNextPage: true`;
|
|
1316
|
+
filtering to captioned attachments returns two and has no next page. Pagination uses the same
|
|
1317
|
+
`first`/`after` and `last`/`before` arguments as [other connections](https://pothos-graphql.dev/docs/plugins/drizzle/connections#page-through-published-posts).
|
|
775
1318
|
|
|
776
|
-
|
|
1319
|
+
### Querying the rows in a resolver
|
|
777
1320
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
1321
|
+
The same helper can build a query for a resolver that fetches the attachment rows itself.
|
|
1322
|
+
Pass GraphQL resolve `info` to `getQuery`, merge any additional filter with its cursor filter,
|
|
1323
|
+
and pass the result to `resolve`:
|
|
1324
|
+
|
|
1325
|
+
```ts
|
|
1326
|
+
// Alternative resolver for the Post.attachments connection above:
|
|
1327
|
+
resolve: async (post, args, ctx, info) => {
|
|
1328
|
+
const query = attachments.getQuery(args, ctx, info);
|
|
1329
|
+
const attachmentRows = await db.query.postMedia.findMany({
|
|
1330
|
+
...query,
|
|
1331
|
+
where: {
|
|
1332
|
+
AND: [query.where ?? {}, { postId: post.id }],
|
|
783
1333
|
},
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
return attachments.resolve(attachmentRows, args, ctx);
|
|
1337
|
+
},
|
|
1338
|
+
```
|
|
1339
|
+
|
|
1340
|
+
This alternative needs `db` imported from the application's database module and `id` selected
|
|
1341
|
+
on Post. Remove the field's relation `select` when using it, so the rows are not also loaded
|
|
1342
|
+
through the parent query. Replacing the helper's `where` rather than combining it would discard
|
|
1343
|
+
pagination constraints.
|
|
1344
|
+
|
|
1345
|
+
If the rows and nodes are the same type, omit `resolveNode` and the node-mapping `select` when
|
|
1346
|
+
creating the helper. Its `ref` provides the node type for `t.connection`. Prefer
|
|
1347
|
+
`t.relatedConnection` when you can expose that relation directly without custom loading.
|
|
1348
|
+
|
|
1349
|
+
## Async selections
|
|
1350
|
+
|
|
1351
|
+
Pothos selections are synchronous by default. Set `AsyncSelections: true` when a selection or
|
|
1352
|
+
relation query must await request data before returning its query options.
|
|
1353
|
+
|
|
1354
|
+
### Async selections
|
|
1355
|
+
|
|
1356
|
+
Use the client and relations configured in [Setup](https://pothos-graphql.dev/docs/plugins/drizzle/setup). The context method below supplies
|
|
1357
|
+
a preview size loaded asynchronously, for example from account settings:
|
|
1358
|
+
|
|
1359
|
+
```ts
|
|
1360
|
+
const builder = new SchemaBuilder<{
|
|
1361
|
+
DrizzleRelations: typeof relations;
|
|
1362
|
+
AsyncSelections: true;
|
|
1363
|
+
Context: {
|
|
1364
|
+
previewSize: () => Promise<number>;
|
|
1365
|
+
};
|
|
1366
|
+
}>({
|
|
1367
|
+
plugins: [DrizzlePlugin],
|
|
1368
|
+
drizzle: {
|
|
1369
|
+
client: db,
|
|
1370
|
+
getTableConfig,
|
|
1371
|
+
relations,
|
|
1372
|
+
},
|
|
1373
|
+
});
|
|
1374
|
+
```
|
|
1375
|
+
|
|
1376
|
+
With the opt-in, `select` functions, relation `query` callbacks, `relatedCount` `where` callbacks,
|
|
1377
|
+
and the `select` and `query` callbacks of `drizzleConnectionHelpers` may be async. Without it they
|
|
1378
|
+
are typed as synchronous, and an async callback is a type error.
|
|
1379
|
+
|
|
1380
|
+
Pothos waits for these callbacks before executing the planned query. `t.relation`, `t.relatedCount`,
|
|
1381
|
+
`t.drizzleField`, `t.drizzleConnection` and `t.relatedConnection` settle their plan before the
|
|
1382
|
+
resolver runs, and need no changes.
|
|
1383
|
+
|
|
1384
|
+
```ts
|
|
1385
|
+
builder.drizzleObject('users', {
|
|
1386
|
+
name: 'User',
|
|
1387
|
+
fields: (t) => ({
|
|
1388
|
+
previewPosts: t.field({
|
|
1389
|
+
type: ['posts'],
|
|
1390
|
+
select: async (args, ctx, nestedSelection) => {
|
|
1391
|
+
return {
|
|
1392
|
+
with: {
|
|
1393
|
+
posts: await nestedSelection({
|
|
1394
|
+
where: { published: true },
|
|
1395
|
+
orderBy: { id: 'asc' },
|
|
1396
|
+
limit: await ctx.previewSize(),
|
|
1397
|
+
}),
|
|
1398
|
+
},
|
|
1399
|
+
};
|
|
1400
|
+
},
|
|
1401
|
+
resolve: (user) => user.posts,
|
|
1402
|
+
}),
|
|
784
1403
|
}),
|
|
785
|
-
resolveNode: (userRole) => userRole.role,
|
|
786
1404
|
});
|
|
1405
|
+
```
|
|
787
1406
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
1407
|
+
### Await nested selections
|
|
1408
|
+
|
|
1409
|
+
`await` what `nestedSelection` returns before putting it in the selection, and the same for the
|
|
1410
|
+
`nestedQuery` passed as the fourth argument of a `t.relatedField` `select`. A selection that
|
|
1411
|
+
contains the promise itself will throw, and so will a `select` that returns while a nested
|
|
1412
|
+
selection it started is still pending. Include the nested selection in the returned query to
|
|
1413
|
+
load its data.
|
|
1414
|
+
|
|
1415
|
+
### Connection helpers
|
|
1416
|
+
|
|
1417
|
+
Pass `awaitSelections: true` to `drizzleConnectionHelpers(...).getQuery`, and `await` the query it
|
|
1418
|
+
returns. Without it, an async selection beneath the field throws, and whether there is one
|
|
1419
|
+
depends on the incoming document rather than on the callback you wrote. A connection helper also
|
|
1420
|
+
throws when its own `select` or `query` is async, whatever the document asked for:
|
|
1421
|
+
|
|
1422
|
+
```ts
|
|
1423
|
+
select: async (args, ctx, nestedSelection) => {
|
|
1424
|
+
return {
|
|
1425
|
+
with: {
|
|
1426
|
+
attachments: await attachments.getQuery(args, ctx, nestedSelection, {
|
|
1427
|
+
awaitSelections: true,
|
|
1428
|
+
}),
|
|
805
1429
|
},
|
|
806
|
-
}
|
|
807
|
-
}
|
|
1430
|
+
};
|
|
1431
|
+
},
|
|
1432
|
+
```
|
|
1433
|
+
|
|
1434
|
+
`awaitSelections` is a per-call option, and is available whether or not the schema sets
|
|
1435
|
+
`AsyncSelections`.
|
|
1436
|
+
|
|
1437
|
+
When `AsyncSelections: true`, also `await` a connection helper's `resolve()` before inspecting or
|
|
1438
|
+
spreading its result: an async `query` callback makes resolution asynchronous. Returning its
|
|
1439
|
+
result directly from a GraphQL resolver remains supported.
|
|
1440
|
+
|
|
1441
|
+
The connection-helper excerpt uses the Post attachments relation and `attachments` helper from
|
|
1442
|
+
[Connection helpers](https://pothos-graphql.dev/docs/plugins/drizzle/connection-helpers). Request-context
|
|
1443
|
+
methods must be supplied by the application; they are not created by declaring the Context type.
|