@pothos/plugin-drizzle 0.19.0 → 0.19.2
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 +19 -0
- package/README.md +879 -806
- package/dts/types.d.ts +4 -4
- package/dts/types.d.ts.map +1 -1
- package/dts/utils/adapter.d.ts +1 -1
- package/dts/utils/adapter.d.ts.map +1 -1
- package/dts/utils/config.d.ts +4 -4
- package/dts/utils/cursors.d.ts.map +1 -1
- package/esm/connection-helpers.js +1 -1
- package/esm/connection-helpers.js.map +1 -1
- package/esm/types.d.ts +4 -4
- package/esm/types.d.ts.map +1 -1
- package/esm/types.js.map +1 -1
- package/esm/utils/adapter.d.ts +1 -1
- package/esm/utils/adapter.d.ts.map +1 -1
- package/esm/utils/adapter.js +12 -2
- package/esm/utils/adapter.js.map +1 -1
- package/esm/utils/config.d.ts +4 -4
- package/esm/utils/cursors.d.ts.map +1 -1
- package/esm/utils/cursors.js +21 -3
- package/esm/utils/cursors.js.map +1 -1
- package/lib/connection-helpers.js +1 -1
- package/lib/connection-helpers.js.map +1 -1
- package/lib/types.js.map +1 -1
- package/lib/utils/adapter.js +12 -2
- package/lib/utils/adapter.js.map +1 -1
- package/lib/utils/cursors.js +21 -3
- package/lib/utils/cursors.js.map +1 -1
- package/package.json +6 -6
- package/src/connection-helpers.ts +1 -1
- package/src/types.ts +8 -7
- package/src/utils/adapter.ts +12 -3
- package/src/utils/cursors.ts +32 -3
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Drizzle Plugin for Pothos
|
|
2
2
|
|
|
3
3
|
> **Note:**
|
|
4
|
-
|
|
4
|
+
This plugin uses drizzle's [relational query builder v2](https://orm.drizzle.team/docs/rqb-v2),
|
|
5
5
|
which ships in `drizzle-orm` 1.0. That release is still a release candidate, and npm's `latest`
|
|
6
6
|
tag is the 0.x line, so install drizzle with the `rc` tag. Its API can still change before 1.0 is
|
|
7
7
|
final.
|
|
@@ -11,133 +11,118 @@
|
|
|
11
11
|
Pothos specific changes.
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
The Drizzle plugin
|
|
15
|
-
|
|
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
16
|
|
|
17
|
-
|
|
17
|
+
The examples use authors and posts to demonstrate the plugin APIs.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
### Getting started
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
Define an object type for a table, expose the columns you want in your API, and add the relations
|
|
26
|
-
you want clients to query. This example uses the builder, database client, and `userId` context
|
|
27
|
-
from Setup, with a users table related to posts:
|
|
28
|
-
|
|
29
|
-
```ts
|
|
30
|
-
const User = builder.drizzleObject('users', {
|
|
31
|
-
name: 'User',
|
|
32
|
-
fields: (t) => ({
|
|
33
|
-
firstName: t.exposeString('firstName'),
|
|
34
|
-
lastName: t.exposeString('lastName'),
|
|
35
|
-
posts: t.relation('posts'),
|
|
36
|
-
}),
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
builder.drizzleObject('posts', {
|
|
40
|
-
name: 'Post',
|
|
41
|
-
fields: (t) => ({ title: t.exposeString('title') }),
|
|
42
|
-
});
|
|
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:
|
|
43
23
|
|
|
24
|
+
```typescript
|
|
44
25
|
builder.queryType({
|
|
45
26
|
fields: (t) => ({
|
|
46
|
-
|
|
27
|
+
author: t.drizzleField({
|
|
47
28
|
type: 'users',
|
|
48
29
|
nullable: true,
|
|
49
|
-
|
|
50
|
-
|
|
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
|
+
},
|
|
51
38
|
}),
|
|
52
39
|
}),
|
|
53
40
|
});
|
|
54
41
|
```
|
|
55
42
|
|
|
56
|
-
|
|
57
|
-
|
|
43
|
+
```graphql
|
|
44
|
+
query AuthorPage {
|
|
45
|
+
author(id: 1) {
|
|
46
|
+
fullName
|
|
47
|
+
posts {
|
|
48
|
+
title
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
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.
|
|
56
|
+
|
|
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.
|
|
58
62
|
|
|
59
63
|
## Setup
|
|
60
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.
|
|
68
|
+
|
|
61
69
|
### Installing
|
|
62
70
|
|
|
63
71
|
```package-install
|
|
64
|
-
npm install --save @pothos/plugin-drizzle
|
|
72
|
+
npm install --save @pothos/plugin-drizzle
|
|
65
73
|
```
|
|
66
74
|
|
|
67
|
-
The
|
|
68
|
-
|
|
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.
|
|
69
77
|
|
|
70
|
-
|
|
71
|
-
SchemaBuilder with the drizzle plugin. This example uses SQLite through `@libsql/client`;
|
|
72
|
-
install that driver and use the `relations` exported by your application. For another database,
|
|
73
|
-
use its Drizzle driver and matching `getTableConfig` import:
|
|
78
|
+
### Configure the builder
|
|
74
79
|
|
|
75
80
|
```ts
|
|
76
|
-
|
|
77
|
-
import { drizzle } from 'drizzle-orm/libsql';
|
|
78
|
-
// Import the appropriate getTableConfig for your dialect
|
|
79
|
-
import { getTableConfig } from 'drizzle-orm/sqlite-core';
|
|
80
|
-
import SchemaBuilder from '@pothos/core';
|
|
81
|
-
import DrizzlePlugin from '@pothos/plugin-drizzle';
|
|
82
|
-
import { relations } from './db/relations';
|
|
83
|
-
|
|
84
|
-
const client = createClient({ url: 'file:./dev.db' });
|
|
85
|
-
const db = drizzle({ client, relations });
|
|
86
|
-
|
|
87
|
-
type DrizzleRelations = typeof relations;
|
|
81
|
+
// builder.ts
|
|
88
82
|
|
|
89
83
|
export interface PothosTypes {
|
|
90
|
-
DrizzleRelations:
|
|
84
|
+
DrizzleRelations: typeof relations;
|
|
91
85
|
Context: { userId: number };
|
|
92
86
|
}
|
|
93
87
|
|
|
94
|
-
const builder = new SchemaBuilder<PothosTypes>({
|
|
88
|
+
export const builder = new SchemaBuilder<PothosTypes>({
|
|
95
89
|
plugins: [DrizzlePlugin],
|
|
96
|
-
drizzle: {
|
|
97
|
-
client: db, // or (ctx) => db if you want to create a request specific client
|
|
98
|
-
getTableConfig,
|
|
99
|
-
relations,
|
|
100
|
-
},
|
|
90
|
+
drizzle: { client: db, getTableConfig, relations },
|
|
101
91
|
});
|
|
102
92
|
```
|
|
103
93
|
|
|
104
|
-
|
|
105
|
-
|
|
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`.
|
|
106
98
|
|
|
107
|
-
|
|
99
|
+
Use this builder to [define GraphQL types and fields](https://pothos-graphql.dev/docs/plugins/drizzle/objects), then call `builder.toSchema()`.
|
|
108
100
|
|
|
109
|
-
|
|
110
|
-
|
|
101
|
+
### Integration with other plugins
|
|
102
|
+
|
|
103
|
+
Install and register `@pothos/plugin-relay` to use `drizzleNode` and connection fields:
|
|
111
104
|
|
|
112
105
|
```ts
|
|
113
|
-
import { createClient } from '@libsql/client';
|
|
114
|
-
import { drizzle } from 'drizzle-orm/libsql';
|
|
115
|
-
import SchemaBuilder from '@pothos/core';
|
|
116
|
-
import DrizzlePlugin from '@pothos/plugin-drizzle';
|
|
117
|
-
import RelayPlugin from '@pothos/plugin-relay';
|
|
118
|
-
import WithInputPlugin from '@pothos/plugin-with-input';
|
|
119
|
-
import { getTableConfig } from 'drizzle-orm/sqlite-core';
|
|
120
|
-
import { relations } from './db/relations';
|
|
121
|
-
|
|
122
|
-
const client = createClient({ url: 'file:./dev.db' });
|
|
123
|
-
const db = drizzle({ client, relations });
|
|
124
106
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
107
|
+
// In the builder options:
|
|
108
|
+
plugins: [RelayPlugin, DrizzlePlugin],
|
|
109
|
+
relay: { nodesOnConnection: true },
|
|
110
|
+
```
|
|
129
111
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
112
|
+
`nodesOnConnection` adds the `nodes` convenience field used in these guides. Without it,
|
|
113
|
+
select `edges { node { ... } }` instead.
|
|
114
|
+
|
|
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],
|
|
138
123
|
```
|
|
139
124
|
|
|
140
|
-
|
|
125
|
+
### Plugin options
|
|
141
126
|
|
|
142
127
|
- `client`: the drizzle client, or a function returning one from the request context.
|
|
143
128
|
- `getTableConfig`: the `getTableConfig` of your dialect, used to read primary keys and unique
|
|
@@ -151,10 +136,19 @@ const builder = new SchemaBuilder<PothosTypes>({
|
|
|
151
136
|
by default, and the fragment's fields are loaded through [fallback queries](https://pothos-graphql.dev/docs/plugins/drizzle/relations#fallback-queries)
|
|
152
137
|
when it resolves. Set this to `false` to plan deferred selections with the rest of the query.
|
|
153
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
|
+
|
|
154
148
|
## Drizzle Objects
|
|
155
149
|
|
|
156
|
-
Use the builder
|
|
157
|
-
|
|
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.
|
|
158
152
|
|
|
159
153
|
### Defining Objects
|
|
160
154
|
|
|
@@ -175,156 +169,134 @@ You will be able to "expose" any column in the table, and GraphQL fields do not
|
|
|
175
169
|
names of the columns in your database. The returned `User` can be used like any other `ObjectRef`
|
|
176
170
|
in Pothos.
|
|
177
171
|
|
|
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.
|
|
174
|
+
|
|
178
175
|
### Custom fields
|
|
179
176
|
|
|
180
|
-
|
|
181
|
-
|
|
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:
|
|
182
179
|
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
resolve: (user, args, ctx, info) => `${user.firstName} ${user.lastName}`,
|
|
189
|
-
}),
|
|
190
|
-
}),
|
|
191
|
-
});
|
|
180
|
+
```typescript
|
|
181
|
+
fullName: t.string({
|
|
182
|
+
select: { columns: { firstName: true, lastName: true } },
|
|
183
|
+
resolve: (user) => `${user.firstName} ${user.lastName}`,
|
|
184
|
+
}),
|
|
192
185
|
```
|
|
193
186
|
|
|
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.
|
|
190
|
+
|
|
194
191
|
### Drizzle Fields
|
|
195
192
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
object type in your schema:
|
|
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.
|
|
201
197
|
|
|
202
|
-
|
|
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
|
|
203
204
|
builder.queryType({
|
|
204
205
|
fields: (t) => ({
|
|
205
|
-
|
|
206
|
+
author: t.drizzleField({
|
|
206
207
|
type: 'users',
|
|
207
208
|
nullable: true,
|
|
208
|
-
args: {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
resolve: (query, root, args, ctx) =>
|
|
212
|
-
db.query.users.findFirst(
|
|
209
|
+
args: { id: t.arg.int({ required: true }) },
|
|
210
|
+
resolve: (query, _root, args) => {
|
|
211
|
+
return db.query.users.findFirst(
|
|
213
212
|
query({
|
|
214
|
-
where: {
|
|
215
|
-
id: Number.parseInt(args.id, 10),
|
|
216
|
-
},
|
|
213
|
+
where: { id: args.id },
|
|
217
214
|
}),
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
users: t.drizzleField({
|
|
221
|
-
type: ['users'],
|
|
222
|
-
resolve: (query, root, args, ctx) => db.query.users.findMany(query()),
|
|
215
|
+
);
|
|
216
|
+
},
|
|
223
217
|
}),
|
|
224
218
|
}),
|
|
225
219
|
});
|
|
226
220
|
```
|
|
227
221
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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.
|
|
232
227
|
|
|
233
228
|
#### `drizzleFieldWithInput`
|
|
234
229
|
|
|
235
230
|
With the [with-input plugin](https://pothos-graphql.dev/docs/plugins/with-input),
|
|
236
231
|
`t.drizzleFieldWithInput` combines `t.drizzleField` with `t.fieldWithInput`. The `input` fields
|
|
237
232
|
become an input object argument, and the resolver still receives the `query` function as its first
|
|
238
|
-
argument. This
|
|
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):
|
|
239
235
|
|
|
240
236
|
```ts
|
|
241
237
|
builder.queryFields((t) => ({
|
|
242
|
-
|
|
238
|
+
authorWithInput: t.drizzleFieldWithInput({
|
|
243
239
|
type: 'users',
|
|
244
240
|
nullable: true,
|
|
245
241
|
input: {
|
|
246
|
-
id: t.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
|
+
);
|
|
247
250
|
},
|
|
248
|
-
resolve: (query, root, args, ctx) =>
|
|
249
|
-
db.query.users.findFirst(query({ where: { id: Number.parseInt(args.input.id, 10) } })),
|
|
250
251
|
}),
|
|
251
252
|
}));
|
|
252
253
|
```
|
|
253
254
|
|
|
254
255
|
## Relations
|
|
255
256
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
Drizzles relational query builder allows you to define the relationships between your tables. The
|
|
259
|
-
`t.relation` method makes it easy to add fields to your GraphQL API that implement those
|
|
260
|
-
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.
|
|
261
259
|
|
|
262
|
-
|
|
263
|
-
builder.drizzleObject('profiles', {
|
|
264
|
-
name: 'Profile',
|
|
265
|
-
fields: (t) => ({
|
|
266
|
-
bio: t.exposeString('bio'),
|
|
267
|
-
}),
|
|
268
|
-
});
|
|
260
|
+
### Published posts and profiles
|
|
269
261
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
fields: (t) => ({
|
|
273
|
-
title: t.exposeString('title'),
|
|
274
|
-
author: t.relation('author'),
|
|
275
|
-
}),
|
|
276
|
-
});
|
|
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:
|
|
277
264
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
+
},
|
|
284
274
|
}),
|
|
285
|
-
})
|
|
275
|
+
}),
|
|
286
276
|
```
|
|
287
277
|
|
|
288
|
-
The
|
|
289
|
-
|
|
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).
|
|
290
283
|
|
|
291
284
|
### Relation queries
|
|
292
285
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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:
|
|
296
289
|
|
|
297
290
|
```ts
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
offset: t.arg.int(),
|
|
306
|
-
},
|
|
307
|
-
// query callback receives (args, ctx, pathInfo)
|
|
308
|
-
query: (args) => ({
|
|
309
|
-
limit: args.limit ?? 10,
|
|
310
|
-
offset: args.offset ?? 0,
|
|
311
|
-
where: {
|
|
312
|
-
published: true,
|
|
313
|
-
},
|
|
314
|
-
orderBy: {
|
|
315
|
-
updatedAt: 'desc',
|
|
316
|
-
},
|
|
317
|
-
}),
|
|
318
|
-
}),
|
|
319
|
-
drafts: t.relation('posts', {
|
|
320
|
-
query: {
|
|
321
|
-
where: {
|
|
322
|
-
published: false,
|
|
323
|
-
},
|
|
324
|
-
},
|
|
325
|
-
}),
|
|
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' },
|
|
326
298
|
}),
|
|
327
|
-
})
|
|
299
|
+
}),
|
|
328
300
|
```
|
|
329
301
|
|
|
330
302
|
The query API enables you to define args and convert them into parameters that will be passed into
|
|
@@ -358,114 +330,180 @@ not return a row for a parent, because it was deleted since it was loaded, or ne
|
|
|
358
330
|
table, that field rejects with `Model users(1) not found`, where the value in parentheses is the
|
|
359
331
|
key that was looked up.
|
|
360
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:
|
|
350
|
+
|
|
351
|
+
```ts
|
|
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
|
+
},
|
|
367
|
+
});
|
|
368
|
+
```
|
|
369
|
+
|
|
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.
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
### Many-to-many relations
|
|
377
|
+
|
|
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:
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
builder.drizzleObject('posts', {
|
|
383
|
+
name: 'Post',
|
|
384
|
+
select: {},
|
|
385
|
+
fields: (t) => ({
|
|
386
|
+
id: t.exposeID('id'),
|
|
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,
|
|
393
|
+
}),
|
|
394
|
+
}),
|
|
395
|
+
});
|
|
396
|
+
export const Media = builder.drizzleObject('media', {
|
|
397
|
+
name: 'Media',
|
|
398
|
+
fields: (t) => ({ url: t.exposeString('url'), uploadedBy: t.relation('uploadedBy') }),
|
|
399
|
+
});
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
`t.relation` and `t.relatedConnection` expose the Media type directly; no GraphQL type for the
|
|
403
|
+
junction table is needed.
|
|
404
|
+
|
|
361
405
|
### Related field
|
|
362
406
|
|
|
363
407
|
The `t.relatedField` method allows you to define a field based on a relation that uses custom
|
|
364
408
|
selections, including aggregations like counts. This is useful when you want to expose derived data
|
|
365
409
|
from a relation without loading the full related records.
|
|
366
410
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
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:
|
|
370
413
|
|
|
371
414
|
```ts
|
|
372
|
-
import {
|
|
415
|
+
import { and, eq } from 'drizzle-orm';
|
|
416
|
+
import { posts } from './tables';
|
|
373
417
|
|
|
374
418
|
builder.drizzleNode('users', {
|
|
375
419
|
name: 'User',
|
|
376
420
|
id: { column: (user) => user.id },
|
|
377
421
|
fields: (t) => ({
|
|
378
422
|
firstName: t.exposeString('firstName'),
|
|
379
|
-
//
|
|
423
|
+
// Count only this author’s published posts
|
|
380
424
|
postsCount: t.relatedField('posts', {
|
|
381
425
|
type: 'Int',
|
|
382
426
|
// buildFilter creates the correct WHERE clause for the relation
|
|
383
|
-
select: (buildFilter) =>
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
+
},
|
|
388
439
|
resolve: (user) => user.postsCount,
|
|
389
440
|
}),
|
|
390
441
|
}),
|
|
391
442
|
});
|
|
392
443
|
```
|
|
393
444
|
|
|
394
|
-
|
|
395
|
-
relation
|
|
396
|
-
|
|
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.
|
|
397
448
|
|
|
398
449
|
`t.relatedField` also accepts the normal field options (`description`, `deprecationReason`,
|
|
399
450
|
`extensions`, and options added by other plugins like `authScopes`). Its `resolve` may be async,
|
|
400
451
|
and receives the resolve `info` as its fourth argument.
|
|
401
452
|
|
|
402
|
-
|
|
453
|
+
## Selections
|
|
403
454
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
target join.
|
|
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.
|
|
407
457
|
|
|
408
|
-
|
|
458
|
+
### Field selections
|
|
409
459
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
RAW: (target) => sql`${target.published} = true`,
|
|
413
|
-
},
|
|
414
|
-
```
|
|
460
|
+
An exposed column, such as `t.exposeString('firstName')`, contributes its own selection. A custom
|
|
461
|
+
resolver must declare the columns it reads:
|
|
415
462
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
463
|
+
```typescript
|
|
464
|
+
fullName: t.string({
|
|
465
|
+
select: { columns: { firstName: true, lastName: true } },
|
|
466
|
+
resolve: (user) => `${user.firstName} ${user.lastName}`,
|
|
467
|
+
}),
|
|
468
|
+
```
|
|
421
469
|
|
|
422
|
-
###
|
|
470
|
+
### Only load a profile when requested
|
|
423
471
|
|
|
424
|
-
|
|
425
|
-
|
|
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:
|
|
426
474
|
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
// Simple count of all related comments
|
|
434
|
-
commentsCount: t.relatedCount('comments'),
|
|
435
|
-
// Count with a where filter
|
|
436
|
-
publishedPostsCount: t.relatedCount('posts', {
|
|
437
|
-
where: eq(posts.published, true),
|
|
438
|
-
}),
|
|
439
|
-
}),
|
|
440
|
-
});
|
|
475
|
+
```typescript
|
|
476
|
+
bio: t.string({
|
|
477
|
+
nullable: true,
|
|
478
|
+
select: { with: { profile: true } },
|
|
479
|
+
resolve: (user) => user.profile?.bio,
|
|
480
|
+
}),
|
|
441
481
|
```
|
|
442
482
|
|
|
443
|
-
|
|
444
|
-
|
|
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.
|
|
445
485
|
|
|
446
|
-
|
|
447
|
-
publishedPostsCount: t.relatedCount('posts', {
|
|
448
|
-
args: {
|
|
449
|
-
category: t.arg.string(),
|
|
450
|
-
},
|
|
451
|
-
where: (args, ctx) => args.category
|
|
452
|
-
? and(eq(posts.published, true), eq(posts.category, args.category))
|
|
453
|
-
: eq(posts.published, true),
|
|
454
|
-
});
|
|
455
|
-
```
|
|
486
|
+
### Computed SQL values
|
|
456
487
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
`totalCount` counts the rows the connection pages over instead, which is one per junction row,
|
|
460
|
-
since that is what the relational query builder returns for the relation.
|
|
488
|
+
The same mechanism supports SQL expressions through `extras`. For example, a field can select
|
|
489
|
+
`lowercaseName` only when the GraphQL operation requests it:
|
|
461
490
|
|
|
462
|
-
|
|
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
|
+
```
|
|
463
501
|
|
|
464
502
|
### Type selections
|
|
465
503
|
|
|
466
|
-
By default, a `drizzleObject` gives its resolvers access to all columns of the table.
|
|
467
|
-
|
|
468
|
-
|
|
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:
|
|
469
507
|
|
|
470
508
|
```ts
|
|
471
509
|
const User = builder.drizzleObject('users', {
|
|
@@ -482,452 +520,189 @@ const User = builder.drizzleObject('users', {
|
|
|
482
520
|
lowercaseName: (users, { sql }) => sql<string>`lower(${users.firstName})`
|
|
483
521
|
},
|
|
484
522
|
},
|
|
485
|
-
fields: (t) =>
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
+
};
|
|
536
|
+
},
|
|
497
537
|
});
|
|
498
538
|
```
|
|
499
539
|
|
|
500
540
|
Any selections added to the type will be available to consume in all resolvers. Columns that are not
|
|
501
541
|
selected can still be exposed as before.
|
|
502
542
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
select the columns that are required to fulfill a specific field. You can do this by adding the
|
|
507
|
-
appropriate selections on each field. Replace the preceding User definition with:
|
|
508
|
-
|
|
509
|
-
```ts
|
|
510
|
-
const User = builder.drizzleObject('users', {
|
|
511
|
-
name: 'User',
|
|
512
|
-
select: {},
|
|
513
|
-
fields: (t) => ({
|
|
514
|
-
fullName: t.string({
|
|
515
|
-
select: {
|
|
516
|
-
columns: { firstName: true, lastName: true },
|
|
517
|
-
},
|
|
518
|
-
resolve: (user, args, ctx, info) => `${user.firstName} ${user.lastName}`,
|
|
519
|
-
}),
|
|
520
|
-
bio: t.string({
|
|
521
|
-
nullable: true,
|
|
522
|
-
select: {
|
|
523
|
-
with: { profile: true },
|
|
524
|
-
},
|
|
525
|
-
resolve: (user) => user.profile?.bio,
|
|
526
|
-
}),
|
|
527
|
-
lowercaseName: t.string({
|
|
528
|
-
select: {
|
|
529
|
-
extras: {
|
|
530
|
-
lowercaseName: (users, { sql }) => sql<string>`lower(${users.firstName})`
|
|
531
|
-
},
|
|
532
|
-
},
|
|
533
|
-
resolve: (user) => user.lowercaseName,
|
|
534
|
-
}),
|
|
535
|
-
}),
|
|
536
|
-
});
|
|
537
|
-
```
|
|
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).
|
|
538
546
|
|
|
539
547
|
## Connections
|
|
540
548
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
### Related connections
|
|
545
|
-
|
|
546
|
-
To implement a relation as a connection, you can use `t.relatedConnection` instead of `t.relation`:
|
|
547
|
-
|
|
548
|
-
```ts
|
|
549
|
-
builder.drizzleNode('users', {
|
|
550
|
-
name: 'User',
|
|
551
|
-
id: { column: (user) => user.id },
|
|
552
|
-
fields: (t) => ({
|
|
553
|
-
posts: t.relatedConnection('posts'),
|
|
554
|
-
}),
|
|
555
|
-
});
|
|
556
|
-
```
|
|
557
|
-
|
|
558
|
-
This will automatically define the `Connection`, and `Edge` types, and their respective fields. To
|
|
559
|
-
customize the Connection and Edge types, options for these types can be passed as additional
|
|
560
|
-
arguments to `t.relatedConnection`, just like `t.connection` from the relay plugin. See the
|
|
561
|
-
[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.
|
|
562
552
|
|
|
563
|
-
|
|
564
|
-
|
|
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).
|
|
565
555
|
|
|
566
|
-
|
|
567
|
-
performed in reverse order, which requires inverting the orderBy clause. To do this automatically,
|
|
568
|
-
the `t.relatedConnection` method accepts orderBy as an object keyed by column name with `'asc'` or
|
|
569
|
-
`'desc'` values, like `{ createdAt: 'desc' }`, rather than using the `asc(column)` and
|
|
570
|
-
`desc(column)` helpers from drizzle. orderBy can also be returned as a single column, or an array of
|
|
571
|
-
columns when ordering by multiple columns, which orders ascending.
|
|
556
|
+
### Page through published posts
|
|
572
557
|
|
|
573
|
-
|
|
574
|
-
|
|
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:
|
|
575
560
|
|
|
576
|
-
```
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
},
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
},
|
|
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' },
|
|
589
574
|
}),
|
|
590
|
-
|
|
591
|
-
}
|
|
592
|
-
})
|
|
575
|
+
);
|
|
576
|
+
},
|
|
577
|
+
}),
|
|
593
578
|
```
|
|
594
579
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
```ts
|
|
600
|
-
builder.drizzleNode('users', {
|
|
601
|
-
name: 'User',
|
|
602
|
-
id: { column: (user) => user.id },
|
|
603
|
-
fields: (t) => ({
|
|
604
|
-
posts: t.relatedConnection('posts', {
|
|
605
|
-
totalCount: true,
|
|
606
|
-
query: () => ({
|
|
607
|
-
where: {
|
|
608
|
-
published: true,
|
|
609
|
-
},
|
|
610
|
-
orderBy: {
|
|
611
|
-
id: 'desc',
|
|
612
|
-
},
|
|
613
|
-
}),
|
|
614
|
-
}),
|
|
615
|
-
}),
|
|
616
|
-
});
|
|
617
|
-
```
|
|
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.
|
|
618
584
|
|
|
619
|
-
|
|
620
|
-
executed when the `totalCount` field is actually requested in the GraphQL query, and it's included
|
|
621
|
-
as a subquery in the main database query for efficiency.
|
|
585
|
+
A client requests the first page with `first`:
|
|
622
586
|
|
|
623
587
|
```graphql
|
|
624
|
-
query {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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
|
|
632
597
|
}
|
|
633
598
|
}
|
|
634
599
|
}
|
|
600
|
+
pageInfo {
|
|
601
|
+
endCursor
|
|
602
|
+
hasNextPage
|
|
603
|
+
}
|
|
635
604
|
}
|
|
636
605
|
}
|
|
637
606
|
```
|
|
638
607
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
regardless of the filter, set `filterConnectionTotalCount: false` in the `drizzle` plugin options:
|
|
642
|
-
|
|
643
|
-
```ts
|
|
644
|
-
const builder = new SchemaBuilder<PothosTypes>({
|
|
645
|
-
plugins: [RelayPlugin, DrizzlePlugin],
|
|
646
|
-
drizzle: {
|
|
647
|
-
client: db,
|
|
648
|
-
getTableConfig,
|
|
649
|
-
relations,
|
|
650
|
-
// count every related row for totalCount, ignoring the where from query (defaults to true)
|
|
651
|
-
filterConnectionTotalCount: false,
|
|
652
|
-
},
|
|
653
|
-
});
|
|
654
|
-
```
|
|
655
|
-
|
|
656
|
-
A `where` on the relation itself always applies to the count, as does the junction table of a
|
|
657
|
-
many-to-many relation defined with `.through(...)`. The count joins the junction table the same way
|
|
658
|
-
the rows do, so a row that matches the junction twice counts twice, and appears twice in the
|
|
659
|
-
connection.
|
|
660
|
-
|
|
661
|
-
### Drizzle connections
|
|
662
|
-
|
|
663
|
-
Similar to `t.drizzleField`, `t.drizzleConnection` allows you to define a connection field that acts
|
|
664
|
-
as an entry point to your drizzle query. The `orderBy` in `t.drizzleConnection` works the same way
|
|
665
|
-
as it does for `t.relatedConnection`
|
|
666
|
-
|
|
667
|
-
```ts
|
|
668
|
-
builder.queryFields((t) => ({
|
|
669
|
-
posts: t.drizzleConnection({
|
|
670
|
-
type: 'posts',
|
|
671
|
-
resolve: (query, root, args, ctx) =>
|
|
672
|
-
db.query.posts.findMany(
|
|
673
|
-
query({
|
|
674
|
-
where: {
|
|
675
|
-
published: true,
|
|
676
|
-
},
|
|
677
|
-
orderBy: {
|
|
678
|
-
id: 'desc',
|
|
679
|
-
},
|
|
680
|
-
}),
|
|
681
|
-
),
|
|
682
|
-
}),
|
|
683
|
-
}));
|
|
684
|
-
```
|
|
685
|
-
|
|
686
|
-
#### drizzleConnection totalCount
|
|
687
|
-
|
|
688
|
-
You can add a `totalCount` field to a `drizzleConnection` by providing a `totalCount` callback function that returns the count:
|
|
689
|
-
|
|
690
|
-
```ts
|
|
691
|
-
builder.queryFields((t) => ({
|
|
692
|
-
posts: t.drizzleConnection({
|
|
693
|
-
type: 'posts',
|
|
694
|
-
// Use db.$count() for a simple count query
|
|
695
|
-
totalCount: () => db.$count(posts, eq(posts.published, true)),
|
|
696
|
-
resolve: (query, root, args, ctx) =>
|
|
697
|
-
db.query.posts.findMany(
|
|
698
|
-
query({
|
|
699
|
-
where: {
|
|
700
|
-
published: true,
|
|
701
|
-
},
|
|
702
|
-
orderBy: {
|
|
703
|
-
id: 'desc',
|
|
704
|
-
},
|
|
705
|
-
}),
|
|
706
|
-
),
|
|
707
|
-
}),
|
|
708
|
-
}));
|
|
709
|
-
```
|
|
710
|
-
|
|
711
|
-
The `totalCount` callback receives the same arguments as a normal resolver (`parent`, `args`, `context`, `info`), allowing you to implement custom count logic based on the query context. The example above uses `db.$count()` for a simple count, but you can use any Drizzle query approach.
|
|
712
|
-
|
|
713
|
-
When only the `totalCount` field is requested (without `edges` or `nodes`), the main query is skipped entirely and only the count query is executed for efficiency.
|
|
714
|
-
|
|
715
|
-
#### Indirect relations as connections
|
|
716
|
-
|
|
717
|
-
In many cases, you can define many to many connections via drizzle relations, allowing the `relatedConnection` API to work across
|
|
718
|
-
more complex relations. In some cases you may want to define a connection for a relation not expressed directly as a relation in
|
|
719
|
-
your drizzle schema. For these cases, you can use the `drizzleConnectionHelpers`, which allows you to define connection with the `t.connection` API.
|
|
720
|
-
|
|
721
|
-
```typescript
|
|
722
|
-
// Create a drizzle object for the node type of your connection
|
|
723
|
-
const Role = builder.drizzleObject('roles', {
|
|
724
|
-
name: 'Role',
|
|
725
|
-
fields: (t) => ({
|
|
726
|
-
id: t.exposeID('id'),
|
|
727
|
-
name: t.exposeString('name'),
|
|
728
|
-
}),
|
|
729
|
-
});
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
// Create connection helpers for the userRoles join table. This will allow you
|
|
734
|
-
// to use the normal t.connection with a drizzle type
|
|
735
|
-
const rolesConnection = drizzleConnectionHelpers(builder, 'userRoles', {
|
|
736
|
-
// select the data needed for the nodes
|
|
737
|
-
select: (nestedSelection) => ({
|
|
738
|
-
with: {
|
|
739
|
-
// use nestedSelection to create the correct selection for the node
|
|
740
|
-
role: nestedSelection(),
|
|
741
|
-
},
|
|
742
|
-
}),
|
|
743
|
-
// resolve the node from the returned list item
|
|
744
|
-
resolveNode: (userRole) => userRole.role,
|
|
745
|
-
});
|
|
746
|
-
|
|
747
|
-
builder.drizzleObjectField('users', 'rolesConnection', (t) =>
|
|
748
|
-
t.connection({
|
|
749
|
-
// The type for the Node
|
|
750
|
-
type: Role,
|
|
751
|
-
nodeNullable: true,
|
|
752
|
-
// since we are not using t.relatedConnection we need to manually
|
|
753
|
-
// include the selections for our connection
|
|
754
|
-
select: (args, ctx, nestedSelection) => ({
|
|
755
|
-
with: {
|
|
756
|
-
userRoles: rolesConnection.getQuery(args, ctx, nestedSelection),
|
|
757
|
-
},
|
|
758
|
-
}),
|
|
759
|
-
// This helper takes a list of nodes and formats them for the connection
|
|
760
|
-
resolve: (user, args, ctx) => {
|
|
761
|
-
return rolesConnection.resolve(user.userRoles, args, ctx, user);
|
|
762
|
-
},
|
|
763
|
-
}),
|
|
764
|
-
);
|
|
765
|
-
```
|
|
766
|
-
|
|
767
|
-
The above example assumes that you are paginating a relation to a join table, where the pagination
|
|
768
|
-
args are applied based on the relation to that join table, but the nodes themselves are nested
|
|
769
|
-
deeper.
|
|
770
|
-
|
|
771
|
-
`drizzleConnectionHelpers` can also be used to manually create a connection where the edge and
|
|
772
|
-
connections share the same model, and pagination happens directly on a relation to nodes type (even
|
|
773
|
-
if that relation is nested).
|
|
774
|
-
|
|
775
|
-
```ts
|
|
776
|
-
const commentConnectionHelpers = drizzleConnectionHelpers(builder, 'comments');
|
|
777
|
-
|
|
778
|
-
const SelectPost = builder.drizzleObject('posts', {
|
|
779
|
-
fields: (t) => ({
|
|
780
|
-
title: t.exposeString('title'),
|
|
781
|
-
comments: t.connection({
|
|
782
|
-
type: commentConnectionHelpers.ref,
|
|
783
|
-
select: (args, ctx, nestedSelection) => ({
|
|
784
|
-
with: {
|
|
785
|
-
comments: commentConnectionHelpers.getQuery(args, ctx, nestedSelection),
|
|
786
|
-
},
|
|
787
|
-
}),
|
|
788
|
-
resolve: (parent, args, ctx) => commentConnectionHelpers.resolve(parent.comments, args, ctx),
|
|
789
|
-
}),
|
|
790
|
-
}),
|
|
791
|
-
});
|
|
792
|
-
```
|
|
793
|
-
|
|
794
|
-
Replace the preceding `rolesConnection` helper and field to add filtering and ordering:
|
|
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`:
|
|
795
610
|
|
|
796
|
-
```
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
// define an order
|
|
802
|
-
orderBy: {
|
|
803
|
-
roleId: 'asc',
|
|
804
|
-
},
|
|
805
|
-
// define a filter
|
|
806
|
-
where: {
|
|
807
|
-
accepted: true,
|
|
611
|
+
```graphql
|
|
612
|
+
query PreviousPosts($before: String) {
|
|
613
|
+
posts(last: 2, before: $before) {
|
|
614
|
+
nodes {
|
|
615
|
+
title
|
|
808
616
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
},
|
|
816
|
-
}),
|
|
817
|
-
// resolve the node from the returned list item
|
|
818
|
-
resolveNode: (userRole) => userRole.role,
|
|
819
|
-
});
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
builder.drizzleObjectField('users', 'rolesConnection', (t) =>
|
|
823
|
-
t.connection({
|
|
824
|
-
type: Role,
|
|
825
|
-
nodeNullable: true,
|
|
826
|
-
// add the args from the connection helper to the field
|
|
827
|
-
args: rolesConnection.getArgs(),
|
|
828
|
-
select: (args, ctx, nestedSelection) => ({
|
|
829
|
-
with: {
|
|
830
|
-
userRoles: rolesConnection.getQuery(args, ctx, nestedSelection),
|
|
831
|
-
},
|
|
832
|
-
}),
|
|
833
|
-
resolve: (user, args, ctx) => rolesConnection.resolve(user.userRoles, args, ctx, user),
|
|
834
|
-
}),
|
|
835
|
-
);
|
|
617
|
+
pageInfo {
|
|
618
|
+
startCursor
|
|
619
|
+
hasPreviousPage
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
836
623
|
```
|
|
837
624
|
|
|
838
|
-
|
|
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 }`.
|
|
839
628
|
|
|
840
|
-
|
|
841
|
-
`DateTime` scalar whose output type is `Date`.
|
|
629
|
+
### Related connections
|
|
842
630
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
select: (nestedSelection) => ({
|
|
846
|
-
with: {
|
|
847
|
-
role: nestedSelection(),
|
|
848
|
-
},
|
|
849
|
-
}),
|
|
850
|
-
resolveNode: (userRole) => userRole.role,
|
|
851
|
-
});
|
|
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:
|
|
852
633
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
select: (args, ctx, nestedSelection) => ({
|
|
859
|
-
with: {
|
|
860
|
-
userRoles: rolesConnection.getQuery(args, ctx, nestedSelection),
|
|
861
|
-
},
|
|
862
|
-
}),
|
|
863
|
-
resolve: (user, args, ctx) =>
|
|
864
|
-
rolesConnection.resolve(
|
|
865
|
-
user.userRoles,
|
|
866
|
-
args,
|
|
867
|
-
ctx,
|
|
868
|
-
user,
|
|
869
|
-
),
|
|
870
|
-
},
|
|
871
|
-
{},
|
|
872
|
-
// options for the edge object
|
|
873
|
-
{
|
|
874
|
-
// define the additional fields on the edge object
|
|
875
|
-
fields: (edge) => ({
|
|
876
|
-
createdAt: edge.field({
|
|
877
|
-
type: 'DateTime',
|
|
878
|
-
// the parent shape for edge fields is inferred from the connections resolve function
|
|
879
|
-
resolve: (role) => role.createdAt,
|
|
880
|
-
}),
|
|
881
|
-
}),
|
|
882
|
-
},
|
|
883
|
-
),
|
|
884
|
-
}));
|
|
634
|
+
```typescript
|
|
635
|
+
postsConnection: t.relatedConnection('posts', {
|
|
636
|
+
query: { where: { published: true }, orderBy: { createdAt: 'desc' } },
|
|
637
|
+
totalCount: true,
|
|
638
|
+
}),
|
|
885
639
|
```
|
|
886
640
|
|
|
887
|
-
|
|
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.
|
|
888
645
|
|
|
889
|
-
|
|
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).
|
|
890
648
|
|
|
891
|
-
|
|
649
|
+
### Connection totalCount
|
|
892
650
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
with: {
|
|
897
|
-
role: nestedSelection(),
|
|
898
|
-
},
|
|
899
|
-
}),
|
|
900
|
-
resolveNode: (userRole) => userRole.role,
|
|
901
|
-
});
|
|
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.
|
|
902
654
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
args: {
|
|
908
|
-
userId: t.arg.int({ required: true }),
|
|
909
|
-
},
|
|
910
|
-
resolve: async (_, args, ctx, info) => {
|
|
911
|
-
const query = rolesConnection.getQuery(args, ctx, info);
|
|
912
|
-
const userRoles = await db.query.userRoles.findMany({
|
|
913
|
-
...query,
|
|
914
|
-
where: {
|
|
915
|
-
AND: [query.where ?? {}, { userId: args.userId }],
|
|
916
|
-
},
|
|
917
|
-
});
|
|
918
|
-
return rolesConnection.resolve(userRoles, args, ctx);
|
|
919
|
-
},
|
|
920
|
-
}),
|
|
921
|
-
}));
|
|
922
|
-
```
|
|
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.
|
|
923
659
|
|
|
924
|
-
|
|
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.
|
|
664
|
+
|
|
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.
|
|
925
675
|
|
|
926
|
-
|
|
676
|
+
## Ordering and cursors
|
|
927
677
|
|
|
928
678
|
Connections page with a cursor that records where in the ordering the previous page ended. This
|
|
929
679
|
applies to `t.relatedConnection`, `t.drizzleConnection`, and `drizzleConnectionHelpers`.
|
|
930
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
|
+
|
|
931
706
|
#### Unique orderings
|
|
932
707
|
|
|
933
708
|
`orderBy: { createdAt: 'desc' }` does not describe a unique ordering. Rows with the same timestamp
|
|
@@ -953,9 +728,9 @@ all marked `notNull()`, are used as provided. Nullable unique columns are not tr
|
|
|
953
728
|
because rows containing a `null` are still tied with each other. Uniqueness declared with
|
|
954
729
|
`uniqueIndex()` is not detected, so those orderings still get the primary key appended.
|
|
955
730
|
|
|
956
|
-
|
|
957
|
-
key columns non-
|
|
958
|
-
|
|
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.
|
|
959
734
|
|
|
960
735
|
Nothing is appended when Pothos cannot find a key it can rely on, which means tables with no primary
|
|
961
736
|
key, and tables whose only unique column is nullable. Those connections keep the ordering you wrote,
|
|
@@ -1004,8 +779,8 @@ mapping:
|
|
|
1004
779
|
builder.queryFields((t) => ({
|
|
1005
780
|
posts: t.drizzleConnection({
|
|
1006
781
|
type: 'posts',
|
|
1007
|
-
resolve: (query) =>
|
|
1008
|
-
db.query.posts.findMany(
|
|
782
|
+
resolve: (query) => {
|
|
783
|
+
return db.query.posts.findMany(
|
|
1009
784
|
query({
|
|
1010
785
|
extras: {
|
|
1011
786
|
createdAtExact: (table) =>
|
|
@@ -1013,7 +788,8 @@ builder.queryFields((t) => ({
|
|
|
1013
788
|
},
|
|
1014
789
|
orderBy: { createdAtExact: 'desc' },
|
|
1015
790
|
}),
|
|
1016
|
-
)
|
|
791
|
+
);
|
|
792
|
+
},
|
|
1017
793
|
}),
|
|
1018
794
|
}));
|
|
1019
795
|
```
|
|
@@ -1041,172 +817,250 @@ The expression also needs to sort in the same order its values compare. A timest
|
|
|
1041
817
|
fixed width UTC text sorts the same way it does chronologically, but a local time or variable width
|
|
1042
818
|
format does not, and will skip rows.
|
|
1043
819
|
|
|
1044
|
-
|
|
1045
|
-
to need one, add an index on the expression.
|
|
1046
|
-
|
|
1047
|
-
## Relay
|
|
820
|
+
## Relay nodes
|
|
1048
821
|
|
|
1049
|
-
|
|
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.
|
|
1050
824
|
|
|
1051
|
-
|
|
1052
|
-
easy to comply with these best practices, the drizzle plugin has built in support for defining relay
|
|
1053
|
-
`nodes` and `connections`.
|
|
825
|
+
### Define a node
|
|
1054
826
|
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
Defining relay nodes works just like defining normal `drizzleObject`s, but requires specifying a
|
|
1058
|
-
column to use as the node's `id` field.
|
|
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:
|
|
1059
829
|
|
|
1060
830
|
```ts
|
|
1061
831
|
builder.drizzleNode('users', {
|
|
1062
832
|
name: 'User',
|
|
1063
|
-
id: {
|
|
1064
|
-
|
|
1065
|
-
// other options for the ID field can be passed here
|
|
1066
|
-
},
|
|
1067
|
-
fields: (t) => ({
|
|
1068
|
-
firstName: t.exposeString('firstName'),
|
|
1069
|
-
lastName: t.exposeString('lastName'),
|
|
1070
|
-
}),
|
|
833
|
+
id: { column: (user) => user.id },
|
|
834
|
+
fields: (t) => ({ firstName: t.exposeString('firstName') }),
|
|
1071
835
|
});
|
|
1072
836
|
```
|
|
1073
837
|
|
|
1074
|
-
|
|
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.
|
|
1075
877
|
|
|
1076
878
|
## Type variants
|
|
1077
879
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
and a registered posts type. The `users` table has a posts relation.
|
|
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.
|
|
1081
882
|
|
|
1082
|
-
|
|
883
|
+
The API has three parts:
|
|
1083
884
|
|
|
1084
|
-
|
|
1085
|
-
|
|
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.
|
|
1086
888
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
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:
|
|
1091
893
|
|
|
1092
894
|
```ts
|
|
1093
|
-
|
|
1094
|
-
export const Viewer = builder.drizzleObject('users', {
|
|
895
|
+
const Viewer = builder.drizzleObject('users', {
|
|
1095
896
|
variant: 'Viewer',
|
|
1096
897
|
select: {},
|
|
1097
|
-
fields: (t) =>
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
published: false,
|
|
1106
|
-
},
|
|
1107
|
-
orderBy: {
|
|
1108
|
-
updatedAt: 'desc',
|
|
1109
|
-
},
|
|
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' },
|
|
1110
906
|
},
|
|
1111
|
-
|
|
1112
|
-
|
|
907
|
+
}),
|
|
908
|
+
};
|
|
909
|
+
},
|
|
1113
910
|
});
|
|
911
|
+
```
|
|
1114
912
|
|
|
1115
|
-
|
|
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:
|
|
918
|
+
|
|
919
|
+
```typescript
|
|
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'),
|
|
1116
924
|
fields: (t) => ({
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
db.query.users.findFirst(
|
|
1122
|
-
query({
|
|
1123
|
-
where: {
|
|
1124
|
-
id: ctx.user.id,
|
|
1125
|
-
},
|
|
1126
|
-
}),
|
|
1127
|
-
),
|
|
925
|
+
user: t.variant('users'),
|
|
926
|
+
email: t.exposeString('email'),
|
|
927
|
+
drafts: t.relation('posts', {
|
|
928
|
+
query: { where: { published: false }, orderBy: { id: 'asc' } },
|
|
1128
929
|
}),
|
|
1129
930
|
}),
|
|
1130
931
|
});
|
|
932
|
+
```
|
|
1131
933
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
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
|
+
```
|
|
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.
|
|
953
|
+
|
|
954
|
+
### Conditionally expose another variant
|
|
955
|
+
|
|
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,
|
|
1141
964
|
}),
|
|
1142
|
-
|
|
965
|
+
);
|
|
1143
966
|
```
|
|
1144
967
|
|
|
1145
|
-
|
|
1146
|
-
|
|
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:
|
|
1147
979
|
|
|
1148
980
|
```ts
|
|
1149
|
-
builder.
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
fields: (t) => ({
|
|
1153
|
-
viewer: t.variant(Viewer, {
|
|
1154
|
-
// loaded with the row when `viewer` is selected
|
|
1155
|
-
select: { columns: { email: true } },
|
|
1156
|
-
isNull: (user, args, ctx) => user.id !== ctx.user?.id,
|
|
1157
|
-
}),
|
|
1158
|
-
}),
|
|
981
|
+
const PostSummary = builder.drizzleObject('posts', {
|
|
982
|
+
variant: 'PostSummary',
|
|
983
|
+
fields: (t) => ({ title: t.exposeString('title') }),
|
|
1159
984
|
});
|
|
985
|
+
|
|
986
|
+
builder.drizzleObjectField('users', 'postSummaries', (t) =>
|
|
987
|
+
t.relation('posts', {
|
|
988
|
+
type: PostSummary,
|
|
989
|
+
query: {
|
|
990
|
+
where: { published: true },
|
|
991
|
+
orderBy: { id: 'asc' },
|
|
992
|
+
},
|
|
993
|
+
}),
|
|
994
|
+
);
|
|
1160
995
|
```
|
|
1161
996
|
|
|
1162
|
-
Two variants
|
|
1163
|
-
|
|
997
|
+
Two variants selected for the same row have their type-level selections merged. Conflicting
|
|
998
|
+
relation arguments or extras can fail; see
|
|
1164
999
|
[Conflicting selections between variants](https://pothos-graphql.dev/docs/plugins/drizzle/query-planning#conflicting-selections-between-variants).
|
|
1165
1000
|
|
|
1166
1001
|
## Interfaces
|
|
1167
1002
|
|
|
1168
1003
|
### Interfaces
|
|
1169
1004
|
|
|
1170
|
-
`builder.drizzleInterface` works
|
|
1171
|
-
|
|
1172
|
-
interface's `select` is planned whenever a field returns the interface, and an implementation's own
|
|
1173
|
-
`select` is planned when a fragment narrows to it. Selections are not inherited, so an
|
|
1174
|
-
implementation that exposes more columns will need a `select` of its own.
|
|
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.
|
|
1175
1007
|
|
|
1176
|
-
|
|
1008
|
+
### Define the interface
|
|
1009
|
+
|
|
1010
|
+
```typescript
|
|
1177
1011
|
export const Viewer = builder.drizzleInterface('users', {
|
|
1178
1012
|
variant: 'Viewer',
|
|
1179
1013
|
select: { columns: { id: true, role: true } },
|
|
1180
|
-
resolveType: (user) => (user.role === '
|
|
1014
|
+
resolveType: (user) => (user.role === 'editor' ? 'EditorViewer' : 'AuthorViewer'),
|
|
1181
1015
|
fields: (t) => ({
|
|
1182
|
-
id: t.exposeID('id'),
|
|
1183
1016
|
user: t.variant('users'),
|
|
1017
|
+
email: t.exposeString('email'),
|
|
1018
|
+
drafts: t.relation('posts', {
|
|
1019
|
+
query: { where: { published: false }, orderBy: { id: 'asc' } },
|
|
1020
|
+
}),
|
|
1184
1021
|
}),
|
|
1185
1022
|
});
|
|
1023
|
+
```
|
|
1024
|
+
|
|
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.
|
|
1186
1027
|
|
|
1028
|
+
### Selecting a viewer implementation
|
|
1029
|
+
|
|
1030
|
+
```typescript
|
|
1187
1031
|
builder.drizzleObject('users', {
|
|
1188
|
-
variant: '
|
|
1032
|
+
variant: 'EditorViewer',
|
|
1189
1033
|
interfaces: [Viewer],
|
|
1190
|
-
select: { columns: {
|
|
1191
|
-
fields: (t) => ({
|
|
1192
|
-
permissions: t.exposeStringList('permissions'),
|
|
1193
|
-
}),
|
|
1034
|
+
select: { columns: { id: true, role: true } },
|
|
1035
|
+
fields: (t) => ({ canReviewSubmissions: t.boolean({ resolve: () => true }) }),
|
|
1194
1036
|
});
|
|
1195
|
-
|
|
1196
1037
|
builder.drizzleObject('users', {
|
|
1197
|
-
variant: '
|
|
1038
|
+
variant: 'AuthorViewer',
|
|
1198
1039
|
interfaces: [Viewer],
|
|
1199
|
-
select: { columns: { id: true } },
|
|
1040
|
+
select: { columns: { id: true, role: true } },
|
|
1200
1041
|
});
|
|
1201
1042
|
```
|
|
1202
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
|
+
|
|
1203
1055
|
Fields can be added to an interface later with `builder.drizzleInterfaceField` and
|
|
1204
1056
|
`builder.drizzleInterfaceFields`, which take the interface ref (or the table name) like their
|
|
1205
1057
|
`drizzleObjectField(s)` counterparts:
|
|
1206
1058
|
|
|
1207
1059
|
```ts
|
|
1208
1060
|
builder.drizzleInterfaceFields(Viewer, (t) => ({
|
|
1209
|
-
|
|
1061
|
+
publishedPosts: t.relatedConnection('posts', {
|
|
1062
|
+
query: { where: { published: true } },
|
|
1063
|
+
}),
|
|
1210
1064
|
}));
|
|
1211
1065
|
```
|
|
1212
1066
|
|
|
@@ -1240,6 +1094,44 @@ the first one planned wins, and the other is loaded with a query of its own. A t
|
|
|
1240
1094
|
is planned before any field's selection, no matter where they appear in the document, and fields
|
|
1241
1095
|
are planned in the order they are selected.
|
|
1242
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
|
|
1106
|
+
}
|
|
1107
|
+
oldest: posts(oldestFirst: true) {
|
|
1108
|
+
title
|
|
1109
|
+
}
|
|
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',
|
|
1130
|
+
},
|
|
1131
|
+
}),
|
|
1132
|
+
}),
|
|
1133
|
+
```
|
|
1134
|
+
|
|
1243
1135
|
### Nested selections
|
|
1244
1136
|
|
|
1245
1137
|
A `select` function receives `nestedSelection`, which plans the selection beneath the field for the
|
|
@@ -1252,11 +1144,19 @@ builder.drizzleObject('users', {
|
|
|
1252
1144
|
name: 'User',
|
|
1253
1145
|
fields: (t) => ({
|
|
1254
1146
|
previewPosts: t.field({
|
|
1255
|
-
type: [
|
|
1256
|
-
select: (args, ctx, nestedSelection) =>
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
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
|
+
};
|
|
1159
|
+
},
|
|
1260
1160
|
resolve: (user) => user.posts,
|
|
1261
1161
|
}),
|
|
1262
1162
|
}),
|
|
@@ -1274,15 +1174,193 @@ which are described under [Relation queries](https://pothos-graphql.dev/docs/plu
|
|
|
1274
1174
|
|
|
1275
1175
|
### Async selections
|
|
1276
1176
|
|
|
1277
|
-
|
|
1278
|
-
|
|
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
|
+
}
|
|
1231
|
+
```
|
|
1232
|
+
|
|
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.
|
|
1237
|
+
|
|
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:
|
|
1244
|
+
|
|
1245
|
+
```typescript
|
|
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 } } : {},
|
|
1255
|
+
}),
|
|
1256
|
+
select: (nestedSelection) => {
|
|
1257
|
+
return {
|
|
1258
|
+
columns: { caption: true },
|
|
1259
|
+
with: { media: nestedSelection() },
|
|
1260
|
+
};
|
|
1261
|
+
},
|
|
1262
|
+
resolveNode: (attachment) => attachment.media,
|
|
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.
|
|
1274
|
+
|
|
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(
|
|
1284
|
+
{
|
|
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
|
+
},
|
|
1297
|
+
},
|
|
1298
|
+
{},
|
|
1299
|
+
{
|
|
1300
|
+
fields: (edge) => ({
|
|
1301
|
+
caption: edge.string({
|
|
1302
|
+
nullable: true,
|
|
1303
|
+
resolve: (attachment) => attachment.caption,
|
|
1304
|
+
}),
|
|
1305
|
+
}),
|
|
1306
|
+
},
|
|
1307
|
+
),
|
|
1308
|
+
);
|
|
1309
|
+
```
|
|
1310
|
+
|
|
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.
|
|
1314
|
+
|
|
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).
|
|
1318
|
+
|
|
1319
|
+
### Querying the rows in a resolver
|
|
1320
|
+
|
|
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 }],
|
|
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:
|
|
1279
1358
|
|
|
1280
1359
|
```ts
|
|
1281
1360
|
const builder = new SchemaBuilder<{
|
|
1282
1361
|
DrizzleRelations: typeof relations;
|
|
1283
1362
|
AsyncSelections: true;
|
|
1284
1363
|
Context: {
|
|
1285
|
-
currentUserId: () => Promise<number>;
|
|
1286
1364
|
previewSize: () => Promise<number>;
|
|
1287
1365
|
};
|
|
1288
1366
|
}>({
|
|
@@ -1299,8 +1377,7 @@ With the opt-in, `select` functions, relation `query` callbacks, `relatedCount`
|
|
|
1299
1377
|
and the `select` and `query` callbacks of `drizzleConnectionHelpers` may be async. Without it they
|
|
1300
1378
|
are typed as synchronous, and an async callback is a type error.
|
|
1301
1379
|
|
|
1302
|
-
|
|
1303
|
-
after every synchronous selection, in document order. `t.relation`, `t.relatedCount`,
|
|
1380
|
+
Pothos waits for these callbacks before executing the planned query. `t.relation`, `t.relatedCount`,
|
|
1304
1381
|
`t.drizzleField`, `t.drizzleConnection` and `t.relatedConnection` settle their plan before the
|
|
1305
1382
|
resolver runs, and need no changes.
|
|
1306
1383
|
|
|
@@ -1308,25 +1385,34 @@ resolver runs, and need no changes.
|
|
|
1308
1385
|
builder.drizzleObject('users', {
|
|
1309
1386
|
name: 'User',
|
|
1310
1387
|
fields: (t) => ({
|
|
1311
|
-
posts: t.relation('posts', {
|
|
1312
|
-
query: async (args, ctx) => ({ where: { authorId: await ctx.currentUserId() } }),
|
|
1313
|
-
}),
|
|
1314
1388
|
previewPosts: t.field({
|
|
1315
|
-
type: [
|
|
1316
|
-
select: async (args, ctx, nestedSelection) =>
|
|
1317
|
-
|
|
1318
|
-
|
|
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
|
+
},
|
|
1319
1401
|
resolve: (user) => user.posts,
|
|
1320
1402
|
}),
|
|
1321
1403
|
}),
|
|
1322
1404
|
});
|
|
1323
1405
|
```
|
|
1324
1406
|
|
|
1407
|
+
### Await nested selections
|
|
1408
|
+
|
|
1325
1409
|
`await` what `nestedSelection` returns before putting it in the selection, and the same for the
|
|
1326
1410
|
`nestedQuery` passed as the fourth argument of a `t.relatedField` `select`. A selection that
|
|
1327
1411
|
contains the promise itself will throw, and so will a `select` that returns while a nested
|
|
1328
|
-
selection it started is still pending.
|
|
1329
|
-
|
|
1412
|
+
selection it started is still pending. Include the nested selection in the returned query to
|
|
1413
|
+
load its data.
|
|
1414
|
+
|
|
1415
|
+
### Connection helpers
|
|
1330
1416
|
|
|
1331
1417
|
Pass `awaitSelections: true` to `drizzleConnectionHelpers(...).getQuery`, and `await` the query it
|
|
1332
1418
|
returns. Without it, an async selection beneath the field throws, and whether there is one
|
|
@@ -1334,13 +1420,15 @@ depends on the incoming document rather than on the callback you wrote. A connec
|
|
|
1334
1420
|
throws when its own `select` or `query` is async, whatever the document asked for:
|
|
1335
1421
|
|
|
1336
1422
|
```ts
|
|
1337
|
-
select: async (args, ctx, nestedSelection) =>
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
}
|
|
1423
|
+
select: async (args, ctx, nestedSelection) => {
|
|
1424
|
+
return {
|
|
1425
|
+
with: {
|
|
1426
|
+
attachments: await attachments.getQuery(args, ctx, nestedSelection, {
|
|
1427
|
+
awaitSelections: true,
|
|
1428
|
+
}),
|
|
1429
|
+
},
|
|
1430
|
+
};
|
|
1431
|
+
},
|
|
1344
1432
|
```
|
|
1345
1433
|
|
|
1346
1434
|
`awaitSelections` is a per-call option, and is available whether or not the schema sets
|
|
@@ -1350,21 +1438,6 @@ When `AsyncSelections: true`, also `await` a connection helper's `resolve()` bef
|
|
|
1350
1438
|
spreading its result: an async `query` callback makes resolution asynchronous. Returning its
|
|
1351
1439
|
result directly from a GraphQL resolver remains supported.
|
|
1352
1440
|
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
under one field, or through a `t.variant` field, the plugin will throw a `PothosValidationError` if
|
|
1357
|
-
their `select` options ask for the same relation with different arguments:
|
|
1358
|
-
|
|
1359
|
-
```
|
|
1360
|
-
PothosValidationError: Type-level selections of Viewer and Admin conflict on relation "posts".
|
|
1361
|
-
Move the relation arguments to a field-level select on one of the types.
|
|
1362
|
-
```
|
|
1363
|
-
|
|
1364
|
-
The same applies to two variants defining the same `extras` key with different functions
|
|
1365
|
-
(`... conflict on extra "lowercaseName"`). Both variants describe one row, so their `select`
|
|
1366
|
-
options are merged into a single query.
|
|
1367
|
-
|
|
1368
|
-
To fix this, move the relation with its arguments, or the extra, into the `select` of the field
|
|
1369
|
-
that needs it on one of the variants. A field-level selection that conflicts with what the row
|
|
1370
|
-
already holds falls back to a query for that field, rather than failing the request.
|
|
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.
|