@pothos/plugin-drizzle 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/CHANGELOG.md +351 -0
  2. package/README.md +784 -221
  3. package/dts/connection-helpers.d.ts +18 -8
  4. package/dts/connection-helpers.d.ts.map +1 -1
  5. package/dts/drizzle-field-builder.d.ts +4 -11
  6. package/dts/drizzle-field-builder.d.ts.map +1 -1
  7. package/dts/global-types.d.ts.map +1 -1
  8. package/dts/index.d.ts.map +1 -1
  9. package/dts/model-loader.d.ts +18 -17
  10. package/dts/model-loader.d.ts.map +1 -1
  11. package/dts/types.d.ts +81 -20
  12. package/dts/types.d.ts.map +1 -1
  13. package/dts/utils/adapter.d.ts +39 -0
  14. package/dts/utils/adapter.d.ts.map +1 -0
  15. package/dts/utils/await-selections.d.ts +12 -0
  16. package/dts/utils/await-selections.d.ts.map +1 -0
  17. package/dts/utils/config.d.ts +23 -3
  18. package/dts/utils/config.d.ts.map +1 -1
  19. package/dts/utils/cursors.d.ts +6 -6
  20. package/dts/utils/cursors.d.ts.map +1 -1
  21. package/dts/utils/map-query.d.ts +22 -21
  22. package/dts/utils/map-query.d.ts.map +1 -1
  23. package/dts/utils/path-info.d.ts +13 -0
  24. package/dts/utils/path-info.d.ts.map +1 -0
  25. package/dts/utils/relation-filter.d.ts +48 -0
  26. package/dts/utils/relation-filter.d.ts.map +1 -0
  27. package/dts/utils/selections.d.ts +2 -21
  28. package/dts/utils/selections.d.ts.map +1 -1
  29. package/esm/connection-helpers.d.ts +18 -8
  30. package/esm/connection-helpers.d.ts.map +1 -1
  31. package/esm/connection-helpers.js +52 -19
  32. package/esm/connection-helpers.js.map +1 -1
  33. package/esm/drizzle-field-builder.d.ts +4 -11
  34. package/esm/drizzle-field-builder.d.ts.map +1 -1
  35. package/esm/drizzle-field-builder.js +138 -96
  36. package/esm/drizzle-field-builder.js.map +1 -1
  37. package/esm/field-builder.js +61 -47
  38. package/esm/field-builder.js.map +1 -1
  39. package/esm/global-types.d.ts.map +1 -1
  40. package/esm/index.d.ts.map +1 -1
  41. package/esm/index.js +37 -61
  42. package/esm/index.js.map +1 -1
  43. package/esm/model-loader.d.ts +18 -17
  44. package/esm/model-loader.d.ts.map +1 -1
  45. package/esm/model-loader.js +76 -45
  46. package/esm/model-loader.js.map +1 -1
  47. package/esm/schema-builder.js +40 -149
  48. package/esm/schema-builder.js.map +1 -1
  49. package/esm/types.d.ts +81 -20
  50. package/esm/types.d.ts.map +1 -1
  51. package/esm/types.js +1 -0
  52. package/esm/types.js.map +1 -1
  53. package/esm/utils/adapter.d.ts +39 -0
  54. package/esm/utils/adapter.d.ts.map +1 -0
  55. package/esm/utils/adapter.js +137 -0
  56. package/esm/utils/adapter.js.map +1 -0
  57. package/esm/utils/await-selections.d.ts +12 -0
  58. package/esm/utils/await-selections.d.ts.map +1 -0
  59. package/esm/utils/await-selections.js +19 -0
  60. package/esm/utils/await-selections.js.map +1 -0
  61. package/esm/utils/config.d.ts +23 -3
  62. package/esm/utils/config.d.ts.map +1 -1
  63. package/esm/utils/config.js +1 -4
  64. package/esm/utils/config.js.map +1 -1
  65. package/esm/utils/cursors.d.ts +6 -6
  66. package/esm/utils/cursors.d.ts.map +1 -1
  67. package/esm/utils/cursors.js +55 -90
  68. package/esm/utils/cursors.js.map +1 -1
  69. package/esm/utils/map-query.d.ts +22 -21
  70. package/esm/utils/map-query.d.ts.map +1 -1
  71. package/esm/utils/map-query.js +40 -336
  72. package/esm/utils/map-query.js.map +1 -1
  73. package/esm/utils/path-info.d.ts +13 -0
  74. package/esm/utils/path-info.d.ts.map +1 -0
  75. package/esm/utils/path-info.js +37 -0
  76. package/esm/utils/path-info.js.map +1 -0
  77. package/esm/utils/relation-filter.d.ts +48 -0
  78. package/esm/utils/relation-filter.d.ts.map +1 -0
  79. package/esm/utils/relation-filter.js +92 -0
  80. package/esm/utils/relation-filter.js.map +1 -0
  81. package/esm/utils/selections.d.ts +2 -21
  82. package/esm/utils/selections.d.ts.map +1 -1
  83. package/esm/utils/selections.js +1 -133
  84. package/esm/utils/selections.js.map +1 -1
  85. package/lib/connection-helpers.js +51 -18
  86. package/lib/connection-helpers.js.map +1 -1
  87. package/lib/drizzle-field-builder.js +136 -94
  88. package/lib/drizzle-field-builder.js.map +1 -1
  89. package/lib/field-builder.js +58 -44
  90. package/lib/field-builder.js.map +1 -1
  91. package/lib/index.js +36 -60
  92. package/lib/index.js.map +1 -1
  93. package/lib/model-loader.js +76 -45
  94. package/lib/model-loader.js.map +1 -1
  95. package/lib/schema-builder.js +40 -149
  96. package/lib/schema-builder.js.map +1 -1
  97. package/lib/types.js +4 -0
  98. package/lib/types.js.map +1 -1
  99. package/lib/utils/adapter.js +147 -0
  100. package/lib/utils/adapter.js.map +1 -0
  101. package/lib/utils/await-selections.js +20 -0
  102. package/lib/utils/await-selections.js.map +1 -0
  103. package/lib/utils/config.js +1 -4
  104. package/lib/utils/config.js.map +1 -1
  105. package/lib/utils/cursors.js +47 -88
  106. package/lib/utils/cursors.js.map +1 -1
  107. package/lib/utils/map-query.js +28 -341
  108. package/lib/utils/map-query.js.map +1 -1
  109. package/lib/utils/path-info.js +39 -0
  110. package/lib/utils/path-info.js.map +1 -0
  111. package/lib/utils/relation-filter.js +91 -0
  112. package/lib/utils/relation-filter.js.map +1 -0
  113. package/lib/utils/selections.js +3 -158
  114. package/lib/utils/selections.js.map +1 -1
  115. package/package.json +8 -6
  116. package/src/connection-helpers.ts +167 -53
  117. package/src/drizzle-field-builder.ts +296 -176
  118. package/src/field-builder.ts +110 -77
  119. package/src/global-types.ts +0 -32
  120. package/src/index.ts +42 -68
  121. package/src/model-loader.ts +110 -71
  122. package/src/schema-builder.ts +76 -169
  123. package/src/types.ts +176 -44
  124. package/src/utils/adapter.ts +197 -0
  125. package/src/utils/await-selections.ts +31 -0
  126. package/src/utils/config.ts +1 -5
  127. package/src/utils/cursors.ts +53 -81
  128. package/src/utils/map-query.ts +51 -708
  129. package/src/utils/path-info.ts +40 -0
  130. package/src/utils/relation-filter.ts +185 -0
  131. package/src/utils/selections.ts +2 -207
  132. package/dts/utils/deep-equal.d.ts +0 -2
  133. package/dts/utils/deep-equal.d.ts.map +0 -1
  134. package/dts/utils/drizzle-graphql.d.ts +0 -2
  135. package/dts/utils/drizzle-graphql.d.ts.map +0 -1
  136. package/dts/utils/loader-map.d.ts +0 -11
  137. package/dts/utils/loader-map.d.ts.map +0 -1
  138. package/dts/utils/usage.d.ts +0 -5
  139. package/dts/utils/usage.d.ts.map +0 -1
  140. package/esm/utils/deep-equal.d.ts +0 -2
  141. package/esm/utils/deep-equal.d.ts.map +0 -1
  142. package/esm/utils/deep-equal.js +0 -45
  143. package/esm/utils/deep-equal.js.map +0 -1
  144. package/esm/utils/drizzle-graphql.d.ts +0 -2
  145. package/esm/utils/drizzle-graphql.d.ts.map +0 -1
  146. package/esm/utils/drizzle-graphql.js +0 -232
  147. package/esm/utils/drizzle-graphql.js.map +0 -1
  148. package/esm/utils/loader-map.d.ts +0 -11
  149. package/esm/utils/loader-map.d.ts.map +0 -1
  150. package/esm/utils/loader-map.js +0 -35
  151. package/esm/utils/loader-map.js.map +0 -1
  152. package/esm/utils/usage.d.ts +0 -5
  153. package/esm/utils/usage.d.ts.map +0 -1
  154. package/esm/utils/usage.js +0 -59
  155. package/esm/utils/usage.js.map +0 -1
  156. package/lib/utils/deep-equal.js +0 -55
  157. package/lib/utils/deep-equal.js.map +0 -1
  158. package/lib/utils/drizzle-graphql.js +0 -236
  159. package/lib/utils/drizzle-graphql.js.map +0 -1
  160. package/lib/utils/loader-map.js +0 -56
  161. package/lib/utils/loader-map.js.map +0 -1
  162. package/lib/utils/usage.js +0 -83
  163. package/lib/utils/usage.js.map +0 -1
  164. package/src/utils/deep-equal.ts +0 -57
  165. package/src/utils/drizzle-graphql.ts +0 -274
  166. package/src/utils/loader-map.ts +0 -49
  167. package/src/utils/usage.ts +0 -62
package/README.md CHANGED
@@ -1,32 +1,94 @@
1
- # Drizzle Plugin
1
+ # Drizzle Plugin for Pothos
2
2
 
3
- ## Installing
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.
8
+
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 defines GraphQL types from your tables and plans database queries from GraphQL
15
+ selections. It loads relations and integrates with Relay nodes and connections.
16
+
17
+ ## Getting started
18
+
19
+ Install the plugin and add it to your builder, as described in [Setup](https://pothos-graphql.dev/docs/plugins/drizzle/setup):
20
+
21
+ ```package-install
22
+ npm install --save @pothos/plugin-drizzle drizzle-orm@rc
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
+ });
43
+
44
+ builder.queryType({
45
+ fields: (t) => ({
46
+ me: t.drizzleField({
47
+ type: 'users',
48
+ nullable: true,
49
+ resolve: (query, root, args, ctx) =>
50
+ db.query.users.findFirst(query({ where: { id: ctx.userId } })),
51
+ }),
52
+ }),
53
+ });
54
+ ```
55
+
56
+ Querying `me { firstName posts { title } }` loads the user and their posts in a single query
57
+ through drizzles relational query builder.
58
+
59
+ ## Setup
60
+
61
+ ### Installing
4
62
 
5
63
  ```package-install
6
- npm install --save @pothos/plugin-drizzle drizzle-orm@beta
64
+ npm install --save @pothos/plugin-drizzle drizzle-orm@rc
7
65
  ```
8
66
 
9
- The drizzle plugin is built on top of drizzles relational query builder, and requires that you
10
- define and configure all the relevant relations in your drizzle schema. See
11
- https://rqbv2.drizzle-orm-fe.pages.dev/docs/relations-v2 for detailed documentation on the relations API.
67
+ The Drizzle plugin uses the relational query builder. Define the tables and relations your schema
68
+ will query; see Drizzle’s [relations API](https://orm.drizzle.team/docs/relations-v2).
12
69
 
13
70
  Once you have configured your drizzle schema, you can initialize your Pothos
14
- SchemaBuilder with the drizzle plugin:
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:
15
74
 
16
75
  ```ts
17
- import { drizzle } from 'drizzle-orm/...';
76
+ import { createClient } from '@libsql/client';
77
+ import { drizzle } from 'drizzle-orm/libsql';
18
78
  // Import the appropriate getTableConfig for your dialect
19
79
  import { getTableConfig } from 'drizzle-orm/sqlite-core';
20
80
  import SchemaBuilder from '@pothos/core';
21
81
  import DrizzlePlugin from '@pothos/plugin-drizzle';
22
82
  import { relations } from './db/relations';
23
83
 
84
+ const client = createClient({ url: 'file:./dev.db' });
24
85
  const db = drizzle({ client, relations });
25
86
 
26
- type DrizzleRelations = typeof relations
87
+ type DrizzleRelations = typeof relations;
27
88
 
28
89
  export interface PothosTypes {
29
90
  DrizzleRelations: DrizzleRelations;
91
+ Context: { userId: number };
30
92
  }
31
93
 
32
94
  const builder = new SchemaBuilder<PothosTypes>({
@@ -39,13 +101,17 @@ const builder = new SchemaBuilder<PothosTypes>({
39
101
  });
40
102
  ```
41
103
 
42
- ### Integration with other plugins
104
+ The examples use an authenticated request context with `userId: number`. Supply it through your
105
+ GraphQL server; see [Context](https://pothos-graphql.dev/docs/guide/context).
106
+
107
+ #### Integration with other plugins
43
108
 
44
109
  The drizzle plugin has integrations with several other plugins. While the `with-input` and `relay`
45
110
  plugins are not required, many examples will assume these plugins have been installed:
46
111
 
47
112
  ```ts
48
- import { drizzle } from 'drizzle-orm/...';
113
+ import { createClient } from '@libsql/client';
114
+ import { drizzle } from 'drizzle-orm/libsql';
49
115
  import SchemaBuilder from '@pothos/core';
50
116
  import DrizzlePlugin from '@pothos/plugin-drizzle';
51
117
  import RelayPlugin from '@pothos/plugin-relay';
@@ -53,10 +119,12 @@ import WithInputPlugin from '@pothos/plugin-with-input';
53
119
  import { getTableConfig } from 'drizzle-orm/sqlite-core';
54
120
  import { relations } from './db/relations';
55
121
 
122
+ const client = createClient({ url: 'file:./dev.db' });
56
123
  const db = drizzle({ client, relations });
57
124
 
58
125
  export interface PothosTypes {
59
126
  DrizzleRelations: typeof relations;
127
+ Context: { userId: number };
60
128
  }
61
129
 
62
130
  const builder = new SchemaBuilder<PothosTypes>({
@@ -69,13 +137,32 @@ const builder = new SchemaBuilder<PothosTypes>({
69
137
  });
70
138
  ```
71
139
 
72
- ## Defining Objects
140
+ #### Plugin options
141
+
142
+ - `client`: the drizzle client, or a function returning one from the request context.
143
+ - `getTableConfig`: the `getTableConfig` of your dialect, used to read primary keys and unique
144
+ constraints.
145
+ - `relations`: the relations passed to `drizzle()`.
146
+ - `defaultConnectionSize` / `maxConnectionSize`: the page size a connection uses when the query
147
+ does not ask for one (defaults to 20), and the largest size it will accept (defaults to 100).
148
+ Both can also be set per field with `defaultSize` and `maxSize`.
149
+ - `filterConnectionTotalCount`: see [Connection totalCount](https://pothos-graphql.dev/docs/plugins/drizzle/connections#connection-totalcount).
150
+ - `skipDeferredFragments`: selections inside a `@defer` fragment are left out of the planned query
151
+ by default, and the fragment's fields are loaded through [fallback queries](https://pothos-graphql.dev/docs/plugins/drizzle/relations#fallback-queries)
152
+ when it resolves. Set this to `false` to plan deferred selections with the rest of the query.
153
+
154
+ ## Drizzle Objects
155
+
156
+ Use the builder and database client from [Setup](https://pothos-graphql.dev/docs/plugins/drizzle/setup). The examples below use a `users` table
157
+ with an integer `id` and string `firstName` and `lastName` columns.
158
+
159
+ ### Defining Objects
73
160
 
74
161
  The `builder.drizzleObject` method can be used to define GraphQL Object types based on a drizzle
75
162
  table:
76
163
 
77
164
  ```ts
78
- const UserRef = builder.drizzleObject('users', {
165
+ const User = builder.drizzleObject('users', {
79
166
  name: 'User',
80
167
  fields: (t) => ({
81
168
  firstName: t.exposeString('firstName'),
@@ -85,16 +172,16 @@ const UserRef = builder.drizzleObject('users', {
85
172
  ```
86
173
 
87
174
  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 `UserRef` can be used like any other `ObjectRef`
175
+ names of the columns in your database. The returned `User` can be used like any other `ObjectRef`
89
176
  in Pothos.
90
177
 
91
- ## Custom fields
178
+ ### Custom fields
92
179
 
93
180
  You will often want to define fields in your API that do not correspond to a specific database
94
- column. To do this, you can define fields with a resolver like any other Pothos object type:
181
+ column. For example, replace the object definition above with a computed full name:
95
182
 
96
183
  ```ts
97
- const UserRef = builder.drizzleObject('users', {
184
+ const User = builder.drizzleObject('users', {
98
185
  name: 'User',
99
186
  fields: (t) => ({
100
187
  fullName: t.string({
@@ -104,81 +191,70 @@ const UserRef = builder.drizzleObject('users', {
104
191
  });
105
192
  ```
106
193
 
107
- ## Type selections
194
+ ### Drizzle Fields
108
195
 
109
- In the above example, you can see that by default we have access to all columns of our table. For
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:
196
+ Drizzle objects and relations allow you to define parts of your schema backed by your drizzle
197
+ schema, but don't provide a clear entry point into this Graph of data. To make your drizzle objects
198
+ queryable, we will need to add fields that return our drizzle objects. This can be done using the
199
+ `t.drizzleField` method. This can be used to define fields on the root `Query` type, or any other
200
+ object type in your schema:
112
201
 
113
202
  ```ts
114
- const UserRef = builder.drizzleObject('users', {
115
- name: 'User',
116
- select: {
117
- columns: {
118
- firstName: true,
119
- lastName: true,
120
- },
121
- with: {
122
- profile: true,
123
- },
124
- extras: {
125
- lowercaseName: (users, sql) => sql<string>`lower(${users.firstName})`
126
- },
127
- },
203
+ builder.queryType({
128
204
  fields: (t) => ({
129
- fullName: t.string({
130
- resolve: (user, args, ctx, info) => `${user.firstName} ${user.lastName}`,
131
- }),
132
- bio: t.string({
133
- resolve: (user) => user.profile.bio,
205
+ user: t.drizzleField({
206
+ type: 'users',
207
+ nullable: true,
208
+ args: {
209
+ id: t.arg.id({ required: true }),
210
+ },
211
+ resolve: (query, root, args, ctx) =>
212
+ db.query.users.findFirst(
213
+ query({
214
+ where: {
215
+ id: Number.parseInt(args.id, 10),
216
+ },
217
+ }),
218
+ ),
134
219
  }),
135
- email: t.string({
136
- resolve: (user) => `${user.lowercaseName}@example.com`,
220
+ users: t.drizzleField({
221
+ type: ['users'],
222
+ resolve: (query, root, args, ctx) => db.query.users.findMany(query()),
137
223
  }),
138
224
  }),
139
225
  });
140
226
  ```
141
227
 
142
- Any selections added to the type will be available to consume in all resolvers. Columns that are not
143
- selected can still be exposed as before.
228
+ The `resolve` function of a `drizzleField` receives a `query` function. Call it and pass its result
229
+ to a Drizzle `findFirst` or `findMany` query. The `query` function optionally accepts any
230
+ arguments that are normally passed into the query, and will merge these options with the selection
231
+ used to resolve data for the nested GraphQL selections.
144
232
 
145
- ## Field selections
233
+ #### `drizzleFieldWithInput`
146
234
 
147
- The previous example allows you to control what gets selected by default, but you often want to only
148
- select the columns that are required to fulfill a specific field. You can do this by adding the
149
- appropriate selections on each field:
235
+ With the [with-input plugin](https://pothos-graphql.dev/docs/plugins/with-input),
236
+ `t.drizzleFieldWithInput` combines `t.drizzleField` with `t.fieldWithInput`. The `input` fields
237
+ become an input object argument, and the resolver still receives the `query` function as its first
238
+ argument. This replaces the `user` field above:
150
239
 
151
240
  ```ts
152
- const UserRef = builder.drizzleObject('users', {
153
- name: 'User',
154
- select: {},
155
- fields: (t) => ({
156
- fullName: t.string({
157
- select: {
158
- columns: { firstName: true, lastName: true },
159
- },
160
- resolve: (user, args, ctx, info) => `${user.firstName} ${user.lastName}`,
161
- }),
162
- bio: t.string({
163
- select: {
164
- with: { profile: true },
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
- }),
241
+ builder.queryFields((t) => ({
242
+ user: t.drizzleFieldWithInput({
243
+ type: 'users',
244
+ nullable: true,
245
+ input: {
246
+ id: t.input.id({ required: true }),
247
+ },
248
+ resolve: (query, root, args, ctx) =>
249
+ db.query.users.findFirst(query({ where: { id: Number.parseInt(args.input.id, 10) } })),
176
250
  }),
177
- });
251
+ }));
178
252
  ```
179
253
 
180
254
  ## Relations
181
255
 
256
+ ### Relations
257
+
182
258
  Drizzles relational query builder allows you to define the relationships between your tables. The
183
259
  `t.relation` method makes it easy to add fields to your GraphQL API that implement those
184
260
  relations:
@@ -212,7 +288,7 @@ builder.drizzleObject('users', {
212
288
  The relation will automatically define GraphQL fields of the appropriate type based on the relation
213
289
  defined in your drizzle schema.
214
290
 
215
- ## Relation queries
291
+ ### Relation queries
216
292
 
217
293
  For some cases, exposing relations as fields without any customization works great, but in some
218
294
  cases you may want to apply some filtering or ordering to your relations. This can be done by
@@ -253,117 +329,42 @@ builder.drizzleObject('users', {
253
329
 
254
330
  The query API enables you to define args and convert them into parameters that will be passed into
255
331
  the relational query builder. The `query` callback receives `(args, ctx, pathInfo)` where `pathInfo`
256
- contains the GraphQL query path information (`path` and `segments`). You can read more about the
257
- relation query builder api [here](https://orm.drizzle.team/docs/rqb#querying)
258
-
259
- ## Drizzle Fields
260
-
261
- Drizzle objects and relations allow you to define parts of your schema backed by your drizzle
262
- schema, but don't provide a clear entry point into this Graph of data. To make your drizzle objects
263
- queryable, we will need to add fields that return our drizzle objects. This can be done using the
264
- `t.drizzleField` method. This can be used to define fields on the root `Query` type, or any other
265
- object type in your schema:
266
-
267
- ```ts
268
- builder.queryType({
269
- fields: (t) => ({
270
- post: t.drizzleField({
271
- type: 'posts',
272
- args: {
273
- id: t.arg.id({ required: true }),
274
- },
275
- resolve: (query, root, args, ctx) =>
276
- db.query.posts.findFirst(
277
- query({
278
- where: {
279
- id: Number.parseInt(args.id, 10),
280
- },
281
- }),
282
- ),
283
- }),
284
- posts: t.drizzleField({
285
- type: ['posts'],
286
- resolve: (query, root, args, ctx) => db.query.posts.findMany(query()),
287
- }),
288
- }),
289
- });
290
- ```
291
-
292
- The `resolve` function of a `drizzleField` will be passed a `query` function that MUST be called and
293
- passed to a drizzle `findOne` or `findMany` query. The `query` function optionally accepts any
294
- arguments that are normally passed into the query, and will merge these options with the selection
295
- used to resolve data for the nested GraphQL selections.
332
+ describes where in the GraphQL query the relation is being loaded:
296
333
 
297
- ## Variants
334
+ - `path`: a list of `ParentType.fieldName` strings, from the root field down to the field being
335
+ resolved (eg. `['Query.user', 'User.posts']`).
336
+ - `segments`: one object per entry in `path`, with `field` (the field name), `alias` (the alias used
337
+ in the query, or the field name if none), `parentType` (the name of the type the field is defined
338
+ on), and `isList` (whether the field returns a list).
298
339
 
299
- It is often useful to be able to define multiple object types based on the same table. This can be
300
- done using a feature called `variants`. The `variants` API consists of 3 parts:
301
-
302
- - A `variant` option that can be passed instead of a name on `drizzleObjects`
303
- - The ability to pass an `ObjectRef` to the `type` option of `t.relation` and other similar fields
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.
340
+ You can read more about the relation query builder api
341
+ [here](https://orm.drizzle.team/docs/rqb-v2)
306
342
 
307
- ```ts
308
- // Viewer type representing the current user
309
- export const Viewer = builder.drizzleObject('users', {
310
- variant: 'Viewer',
311
- select: {},
312
- fields: (t) => ({
313
- id: t.exposeID('id'),
314
- // A reference to the normal user type so normal user fields can be queried
315
- user: t.variant('users'),
316
- // Adding drafts to View allows a user to fetch their own drafts without exposing it for Other Users in the API
317
- drafts: t.relation('posts', {
318
- query: {
319
- where: {
320
- published: false,
321
- },
322
- orderBy: {
323
- updatedAt: 'desc',
324
- },
325
- },
326
- }),
327
- }),
328
- });
343
+ ### Fallback queries
329
344
 
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
- ),
343
- }),
344
- }),
345
- });
345
+ A field whose data is not on the row it resolves from is loaded with a fallback query. This happens
346
+ when:
346
347
 
347
- builder.drizzleNode('users', {
348
- name: 'User',
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
- }),
356
- });
357
- ```
348
+ - The parent row was not loaded through a `t.drizzleField`, `t.relation`, or connection. This
349
+ covers rows a resolver queried itself, and rows that came from somewhere else entirely.
350
+ - A `drizzleField` resolver did not pass the result of `query()` to drizzle.
351
+ - A relation's arguments conflict with a sibling selection of the same relation that was planned
352
+ first.
358
353
 
354
+ Fallback queries are batched. Every row of a table that needs the same selection in the same tick
355
+ is loaded with one `findMany` filtered on the primary key, or on the first unique column for a
356
+ table that has no primary key, and the rows are matched back to their parents. If the query does
357
+ not return a row for a parent, because it was deleted since it was loaded, or never came from the
358
+ table, that field rejects with `Model users(1) not found`, where the value in parentheses is the
359
+ key that was looked up.
359
360
 
360
- ## Related field
361
+ ### Related field
361
362
 
362
363
  The `t.relatedField` method allows you to define a field based on a relation that uses custom
363
364
  selections, including aggregations like counts. This is useful when you want to expose derived data
364
365
  from a relation without loading the full related records.
365
366
 
366
- ### Count aggregations
367
+ #### Count aggregations
367
368
 
368
369
  One common use case is adding a count field that efficiently counts related records:
369
370
 
@@ -394,7 +395,31 @@ The `buildFilter` function passed to `select` generates the appropriate SQL filt
394
395
  relation definition. This is no different than using `t.field`, but the `buildFilter` helper makes
395
396
  it easier to filter for the related records.
396
397
 
397
- ## Related count
398
+ `t.relatedField` also accepts the normal field options (`description`, `deprecationReason`,
399
+ `extensions`, and options added by other plugins like `authScopes`). Its `resolve` may be async,
400
+ and receives the resolve `info` as its fourth argument.
401
+
402
+ #### SQLite many-to-many filters
403
+
404
+ On SQLite, `buildFilter` can look up related target identities through the junction join when the
405
+ target has a non-null unique key. PostgreSQL retains an `EXISTS` predicate to avoid an additional
406
+ target join.
407
+
408
+ For a custom `RAW` scope in a Drizzle relation definition, use the table supplied to the callback:
409
+
410
+ ```ts
411
+ where: {
412
+ RAW: (target) => sql`${target.published} = true`,
413
+ },
414
+ ```
415
+
416
+ This lets Drizzle use the target's alias inside the lookup. A static SQL scope referencing the
417
+ original target table, such as ``RAW: sql`${posts.published} = true` ``, is passed through unchanged
418
+ and can still force SQLite to scan the target table. The same applies to a callback that ignores
419
+ its table argument and references `posts` directly. Object filters such as
420
+ `where: { published: true }` also use the supplied alias.
421
+
422
+ ### Related count
398
423
 
399
424
  For the common case of counting related records, there's a simpler `t.relatedCount` method that handles
400
425
  all the boilerplate for you:
@@ -426,62 +451,124 @@ publishedPostsCount: t.relatedCount('posts', {
426
451
  where: (args, ctx) => args.category
427
452
  ? and(eq(posts.published, true), eq(posts.category, args.category))
428
453
  : eq(posts.published, true),
429
- }),
454
+ });
430
455
  ```
431
456
 
432
- ## Relay integration
457
+ For a many-to-many relation (one defined with `.through(...)`), `t.relatedCount` counts distinct
458
+ related rows, so a row reachable through two junction rows counts once. A `t.relatedConnection`'s
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.
433
461
 
434
- Relay provides some very useful best practices that are useful for most GraphQL APIs. To make it
435
- easy to comply with these best practices, the drizzle plugin has built in support for defining relay
436
- `nodes` and `connections`.
462
+ ## Selections
437
463
 
438
- ## Relay Nodes
464
+ ### Type selections
439
465
 
440
- Defining relay nodes works just like defining normal `drizzleObject`s, but requires specifying a
441
- column to use as the nodes `id` field.
466
+ By default, a `drizzleObject` gives its resolvers access to all columns of the table. For tables
467
+ with many columns, it can be more efficient to only select the needed columns. You can configure the
468
+ selected columns, and relations by passing a `select` option when defining the type:
442
469
 
443
470
  ```ts
444
- builder.drizzleNode('users', {
471
+ const User = builder.drizzleObject('users', {
445
472
  name: 'User',
446
- id: {
447
- column: (user) => user.id,
448
- // other options for the ID field can be passed here
473
+ select: {
474
+ columns: {
475
+ firstName: true,
476
+ lastName: true,
477
+ },
478
+ with: {
479
+ profile: true,
480
+ },
481
+ extras: {
482
+ lowercaseName: (users, { sql }) => sql<string>`lower(${users.firstName})`
483
+ },
449
484
  },
450
485
  fields: (t) => ({
451
- firstName: t.exposeString('firstName'),
452
- lastName: t.exposeString('lastName'),
486
+ fullName: t.string({
487
+ resolve: (user, args, ctx, info) => `${user.firstName} ${user.lastName}`,
488
+ }),
489
+ bio: t.string({
490
+ nullable: true,
491
+ resolve: (user) => user.profile?.bio,
492
+ }),
493
+ lowercaseName: t.string({
494
+ resolve: (user) => user.lowercaseName,
495
+ }),
453
496
  }),
454
497
  });
455
498
  ```
456
499
 
457
- The id column can also be set to a list of columns for types with a composite primary key.
500
+ Any selections added to the type will be available to consume in all resolvers. Columns that are not
501
+ selected can still be exposed as before.
458
502
 
459
- ## Related connections
503
+ ### Field selections
460
504
 
461
- To implement a relation as a connection, you can use `t.relatedConnection` instead of `t.relation`:
505
+ The previous example allows you to control what gets selected by default, but you often want to only
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:
462
508
 
463
509
  ```ts
464
- builder.drizzleNode('users', {
510
+ const User = builder.drizzleObject('users', {
465
511
  name: 'User',
512
+ select: {},
466
513
  fields: (t) => ({
467
- posts: t.relatedConnection('posts'),
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
+ }),
468
535
  }),
469
536
  });
470
537
  ```
471
538
 
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.
539
+ ## Connections
476
540
 
477
- You can also define a `query` like with `t.relation`. The only difference with `t.relatedConnection`
478
- is that the `orderBy` format is slightly changed.
541
+ Use the [builder setup with Relay](https://pothos-graphql.dev/docs/plugins/drizzle/setup#integration-with-other-plugins) and register the users
542
+ and posts object types. Examples defining the same field or helper are alternatives.
479
543
 
480
- To comply with the relay spec and efficiently support backwards pagination, some queries need to be
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.
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.
562
+
563
+ You can also define a `query` like with `t.relation`. The only difference with `t.relatedConnection`
564
+ is that the `orderBy` format is slightly changed.
565
+
566
+ To comply with the relay spec and efficiently support backwards pagination, some queries need to be
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.
485
572
 
486
573
  Ordering defaults to using the table `primaryKey`, and the orderBy columns will also be used to
487
574
  derive the connections cursor.
@@ -489,6 +576,7 @@ derive the connections cursor.
489
576
  ```ts
490
577
  builder.drizzleNode('users', {
491
578
  name: 'User',
579
+ id: { column: (user) => user.id },
492
580
  fields: (t) => ({
493
581
  posts: t.relatedConnection('posts', {
494
582
  query: () => ({
@@ -504,13 +592,14 @@ builder.drizzleNode('users', {
504
592
  });
505
593
  ```
506
594
 
507
- ### Connection totalCount
595
+ #### Connection totalCount
508
596
 
509
597
  You can add a `totalCount` field to your connection by setting the `totalCount` option to `true`:
510
598
 
511
599
  ```ts
512
600
  builder.drizzleNode('users', {
513
601
  name: 'User',
602
+ id: { column: (user) => user.id },
514
603
  fields: (t) => ({
515
604
  posts: t.relatedConnection('posts', {
516
605
  totalCount: true,
@@ -547,7 +636,29 @@ query {
547
636
  }
548
637
  ```
549
638
 
550
- ## Drizzle connections
639
+ The count applies the `where` returned by the field's `query`, so it counts the same rows the
640
+ connection paginates (only published posts in the example above). To count every related row
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
551
662
 
552
663
  Similar to `t.drizzleField`, `t.drizzleConnection` allows you to define a connection field that acts
553
664
  as an entry point to your drizzle query. The `orderBy` in `t.drizzleConnection` works the same way
@@ -572,7 +683,7 @@ builder.queryFields((t) => ({
572
683
  }));
573
684
  ```
574
685
 
575
- ### drizzleConnection totalCount
686
+ #### drizzleConnection totalCount
576
687
 
577
688
  You can add a `totalCount` field to a `drizzleConnection` by providing a `totalCount` callback function that returns the count:
578
689
 
@@ -601,7 +712,7 @@ The `totalCount` callback receives the same arguments as a normal resolver (`par
601
712
 
602
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.
603
714
 
604
- ### Indirect relations as connections
715
+ #### Indirect relations as connections
605
716
 
606
717
  In many cases, you can define many to many connections via drizzle relations, allowing the `relatedConnection` API to work across
607
718
  more complex relations. In some cases you may want to define a connection for a relation not expressed directly as a relation in
@@ -619,7 +730,7 @@ const Role = builder.drizzleObject('roles', {
619
730
 
620
731
 
621
732
 
622
- // Create connection helpers for the media type. This will allow you
733
+ // Create connection helpers for the userRoles join table. This will allow you
623
734
  // to use the normal t.connection with a drizzle type
624
735
  const rolesConnection = drizzleConnectionHelpers(builder, 'userRoles', {
625
736
  // select the data needed for the nodes
@@ -633,10 +744,11 @@ const rolesConnection = drizzleConnectionHelpers(builder, 'userRoles', {
633
744
  resolveNode: (userRole) => userRole.role,
634
745
  });
635
746
 
636
- builder.drizzleObjectField('User', 'rolesConnection', (t) =>
747
+ builder.drizzleObjectField('users', 'rolesConnection', (t) =>
637
748
  t.connection({
638
749
  // The type for the Node
639
750
  type: Role,
751
+ nodeNullable: true,
640
752
  // since we are not using t.relatedConnection we need to manually
641
753
  // include the selections for our connection
642
754
  select: (args, ctx, nestedSelection) => ({
@@ -661,7 +773,7 @@ connections share the same model, and pagination happens directly on a relation
661
773
  if that relation is nested).
662
774
 
663
775
  ```ts
664
- const commentConnectionHelpers = drizzleConnectionHelpers(builder, 'Comment');
776
+ const commentConnectionHelpers = drizzleConnectionHelpers(builder, 'comments');
665
777
 
666
778
  const SelectPost = builder.drizzleObject('posts', {
667
779
  fields: (t) => ({
@@ -679,7 +791,7 @@ const SelectPost = builder.drizzleObject('posts', {
679
791
  });
680
792
  ```
681
793
 
682
- Arguments, ordering and filtering can also be defined in the helpers:
794
+ Replace the preceding `rolesConnection` helper and field to add filtering and ordering:
683
795
 
684
796
  ```ts
685
797
  const rolesConnection = drizzleConnectionHelpers(builder, 'userRoles', {
@@ -689,7 +801,7 @@ const rolesConnection = drizzleConnectionHelpers(builder, 'userRoles', {
689
801
  // define an order
690
802
  orderBy: {
691
803
  roleId: 'asc',
692
- }
804
+ },
693
805
  // define a filter
694
806
  where: {
695
807
  accepted: true,
@@ -707,9 +819,10 @@ const rolesConnection = drizzleConnectionHelpers(builder, 'userRoles', {
707
819
  });
708
820
 
709
821
 
710
- builder.drizzleObjectField('User', 'rolesConnection', (t) =>
822
+ builder.drizzleObjectField('users', 'rolesConnection', (t) =>
711
823
  t.connection({
712
824
  type: Role,
825
+ nodeNullable: true,
713
826
  // add the args from the connection helper to the field
714
827
  args: rolesConnection.getArgs(),
715
828
  select: (args, ctx, nestedSelection) => ({
@@ -722,9 +835,10 @@ builder.drizzleObjectField('User', 'rolesConnection', (t) =>
722
835
  );
723
836
  ```
724
837
 
725
- ### Extending connection edges
838
+ #### Extending connection edges
726
839
 
727
- In some cases you may want to expose some data from an indirect connection on the edge object.
840
+ This alternative exposes the join row’s `createdAt` on each edge. It assumes a registered
841
+ `DateTime` scalar whose output type is `Date`.
728
842
 
729
843
  ```typescript
730
844
  const rolesConnection = drizzleConnectionHelpers(builder, 'userRoles', {
@@ -736,10 +850,11 @@ const rolesConnection = drizzleConnectionHelpers(builder, 'userRoles', {
736
850
  resolveNode: (userRole) => userRole.role,
737
851
  });
738
852
 
739
- builder.drizzleObjectFields('User', (t) => ({
853
+ builder.drizzleObjectFields('users', (t) => ({
740
854
  rolesConnection: t.connection(
741
855
  {
742
856
  type: Role,
857
+ nodeNullable: true,
743
858
  select: (args, ctx, nestedSelection) => ({
744
859
  with: {
745
860
  userRoles: rolesConnection.getQuery(args, ctx, nestedSelection),
@@ -769,7 +884,7 @@ builder.drizzleObjectFields('User', (t) => ({
769
884
  }));
770
885
  ```
771
886
 
772
- ### `drizzleConnectionHelpers` for non-relation connections
887
+ #### `drizzleConnectionHelpers` for non-relation connections
773
888
 
774
889
  You can also use `drizzleConnectionHelpers` for non-relation connections where you want a connection where your edges and nodes are not the same type.
775
890
 
@@ -788,20 +903,468 @@ const rolesConnection = drizzleConnectionHelpers(builder, 'userRoles', {
788
903
  builder.queryFields((t) => ({
789
904
  roles: t.connection({
790
905
  type: Role,
906
+ nodeNullable: true,
791
907
  args: {
792
908
  userId: t.arg.int({ required: true }),
793
909
  },
794
- nodeNullable: true,
795
910
  resolve: async (_, args, ctx, info) => {
796
911
  const query = rolesConnection.getQuery(args, ctx, info);
797
912
  const userRoles = await db.query.userRoles.findMany({
798
913
  ...query,
799
914
  where: {
800
- ...query.where,
801
- userId: args.userId,
915
+ AND: [query.where ?? {}, { userId: args.userId }],
802
916
  },
803
917
  });
804
918
  return rolesConnection.resolve(userRoles, args, ctx);
805
919
  },
806
920
  }),
807
921
  }));
922
+ ```
923
+
924
+ ## Ordering and cursors
925
+
926
+ ### Ordering and cursors
927
+
928
+ Connections page with a cursor that records where in the ordering the previous page ended. This
929
+ applies to `t.relatedConnection`, `t.drizzleConnection`, and `drizzleConnectionHelpers`.
930
+
931
+ #### Unique orderings
932
+
933
+ `orderBy: { createdAt: 'desc' }` does not describe a unique ordering. Rows with the same timestamp
934
+ can be returned in a different order on each query, so paging through the connection may return a row
935
+ twice, or skip it.
936
+
937
+ Pothos adds the primary key to the ordering when the columns you provide are not already unique, so
938
+ this:
939
+
940
+ ```ts
941
+ orderBy: { createdAt: 'desc' }
942
+ ```
943
+
944
+ is queried as `createdAt desc, id desc`. You can add the tie breaker yourself if you want it in a
945
+ different position or direction:
946
+
947
+ ```ts
948
+ orderBy: { createdAt: 'desc', id: 'asc' }
949
+ ```
950
+
951
+ Orderings that already contain the primary key, or a unique column or constraint whose columns are
952
+ all marked `notNull()`, are used as provided. Nullable unique columns are not treated as unique,
953
+ because rows containing a `null` are still tied with each other. Uniqueness declared with
954
+ `uniqueIndex()` is not detected, so those orderings still get the primary key appended.
955
+
956
+ Primary keys are used whether or not their columns are marked `notNull()`, since SQL makes primary
957
+ key columns non-nullable anyway. That includes composite keys declared with
958
+ `primaryKey({ columns: [...] })`.
959
+
960
+ Nothing is appended when Pothos cannot find a key it can rely on, which means tables with no primary
961
+ key, and tables whose only unique column is nullable. Those connections keep the ordering you wrote,
962
+ so give them a tie breaker yourself.
963
+
964
+ #### Nullable ordering columns
965
+
966
+ A row whose ordering value is `null` cannot be paged past. `null` is not greater or less than
967
+ anything in SQL, so no row compares as coming after it and the next page comes back empty. The same
968
+ applies to an ordering expression that can evaluate to `null`.
969
+
970
+ Order by columns that are `notNull()`, or give the expression a real value to fall back to with
971
+ `coalesce`.
972
+
973
+ #### Cursor values and precision
974
+
975
+ A cursor stores the values your driver returned to JavaScript, and those values are compared against
976
+ the column when the next page is requested. If the value in the cursor is not the value the database
977
+ has, the comparison will not match the rows you expect.
978
+
979
+ This comes up with timestamps. `timestamp({ mode: 'date' })` returns a JavaScript `Date`, which only
980
+ holds milliseconds, while a Postgres `timestamptz` column stores microseconds. A row stored at
981
+ `.086068` produces a cursor containing `.086`, and `created_at < '.086'` does not match the other
982
+ rows in that millisecond, so they are never returned on any page.
983
+
984
+ There are two ways to avoid this.
985
+
986
+ The first is to map the column so the full value reaches JavaScript. `mode: 'string'` returns the
987
+ timestamp as text, with the microseconds intact:
988
+
989
+ ```ts
990
+ export const posts = pgTable('posts', {
991
+ id: serial('id').primaryKey(),
992
+ createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).notNull(),
993
+ });
994
+ ```
995
+
996
+ The cursor stores that text, and Postgres parses it back to the value it stored. The column is still
997
+ what gets ordered and compared, so an index on it is still used. The field is typed as a `string`
998
+ rather than a `Date`.
999
+
1000
+ The second is to order by an expression that returns the full value, which lets you keep the `Date`
1001
+ mapping:
1002
+
1003
+ ```ts
1004
+ builder.queryFields((t) => ({
1005
+ posts: t.drizzleConnection({
1006
+ type: 'posts',
1007
+ resolve: (query) =>
1008
+ db.query.posts.findMany(
1009
+ query({
1010
+ extras: {
1011
+ createdAtExact: (table) =>
1012
+ sql`to_char(${table.createdAt} AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')`,
1013
+ },
1014
+ orderBy: { createdAtExact: 'desc' },
1015
+ }),
1016
+ ),
1017
+ }),
1018
+ }));
1019
+ ```
1020
+
1021
+ #### Ordering by an expression
1022
+
1023
+ `orderBy` accepts the name of any extra declared in the same query. Pothos orders by the expression,
1024
+ builds the cursor from the value it returns, and compares the expression when paging:
1025
+
1026
+ ```ts
1027
+ query({
1028
+ extras: { titleLength: (table) => sql`length(${table.title})` },
1029
+ orderBy: { titleLength: 'asc' },
1030
+ })
1031
+ ```
1032
+
1033
+ Extras used this way should be written as a callback. Drizzle aliases the table it queries and passes
1034
+ that alias to the callback, so an expression built from the imported table object will reference a
1035
+ name the query does not have.
1036
+
1037
+ With `drizzleConnectionHelpers`, declare the extra in `query` rather than in `select`. Only `query`
1038
+ is read when the ordering is resolved.
1039
+
1040
+ The expression also needs to sort in the same order its values compare. A timestamp formatted as
1041
+ fixed width UTC text sorts the same way it does chronologically, but a local time or variable width
1042
+ format does not, and will skip rows.
1043
+
1044
+ Ordering by an expression cannot use an index on the underlying column. If the table is large enough
1045
+ to need one, add an index on the expression.
1046
+
1047
+ ## Relay
1048
+
1049
+ ### Relay integration
1050
+
1051
+ Relay provides some very useful best practices that are useful for most GraphQL APIs. To make it
1052
+ easy to comply with these best practices, the drizzle plugin has built in support for defining relay
1053
+ `nodes` and `connections`.
1054
+
1055
+ ### Relay Nodes
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.
1059
+
1060
+ ```ts
1061
+ builder.drizzleNode('users', {
1062
+ name: 'User',
1063
+ id: {
1064
+ column: (user) => user.id,
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
+ }),
1071
+ });
1072
+ ```
1073
+
1074
+ The id column can also be set to a list of columns for types with a composite primary key.
1075
+
1076
+ ## Type variants
1077
+
1078
+ Use the [builder setup with Relay](https://pothos-graphql.dev/docs/plugins/drizzle/setup#integration-with-other-plugins), and include
1079
+ `Context: { user: { id: number } }` in `PothosTypes`. These examples assume an authenticated user
1080
+ and a registered posts type. The `users` table has a posts relation.
1081
+
1082
+ ### Variants
1083
+
1084
+ It is often useful to be able to define multiple object types based on the same table. This can be
1085
+ done using a feature called `variants`. The `variants` API consists of 3 parts:
1086
+
1087
+ - A `variant` option that can be passed instead of a name on `drizzleObjects`
1088
+ - The ability to pass an `ObjectRef` to the `type` option of `t.relation` and other similar fields
1089
+ - A `t.variant` method that works similar to `t.relation`, but is used to define a GraphQL field that
1090
+ references a variant of the same record.
1091
+
1092
+ ```ts
1093
+ // Viewer type representing the current user
1094
+ export const Viewer = builder.drizzleObject('users', {
1095
+ variant: 'Viewer',
1096
+ select: {},
1097
+ fields: (t) => ({
1098
+ id: t.exposeID('id'),
1099
+ // A reference to the normal user type so normal user fields can be queried
1100
+ user: t.variant('users'),
1101
+ // Adding drafts to View allows a user to fetch their own drafts without exposing it for Other Users in the API
1102
+ drafts: t.relation('posts', {
1103
+ query: {
1104
+ where: {
1105
+ published: false,
1106
+ },
1107
+ orderBy: {
1108
+ updatedAt: 'desc',
1109
+ },
1110
+ },
1111
+ }),
1112
+ }),
1113
+ });
1114
+
1115
+ builder.queryType({
1116
+ fields: (t) => ({
1117
+ me: t.drizzleField({
1118
+ // We can use the ref returned by builder.drizzleObject to define our `drizzleField`
1119
+ type: Viewer,
1120
+ resolve: (query, root, args, ctx) =>
1121
+ db.query.users.findFirst(
1122
+ query({
1123
+ where: {
1124
+ id: ctx.user.id,
1125
+ },
1126
+ }),
1127
+ ),
1128
+ }),
1129
+ }),
1130
+ });
1131
+
1132
+ builder.drizzleNode('users', {
1133
+ name: 'User',
1134
+ id: { column: (user) => user.id },
1135
+ fields: (t) => ({
1136
+ firstName: t.exposeString('firstName'),
1137
+ // This field will resolve to the Viewer type, but be set to null if the user is not the current user
1138
+ viewer: t.variant(Viewer, {
1139
+ isNull: (user, args, ctx) => user.id !== ctx.user?.id,
1140
+ }),
1141
+ }),
1142
+ });
1143
+ ```
1144
+
1145
+ As an alternative to the User node definition above, a `t.variant` field can have a `select` of its own. It is planned along with the variant's
1146
+ type-level selection when the variant is queried through that field:
1147
+
1148
+ ```ts
1149
+ builder.drizzleNode('users', {
1150
+ name: 'User',
1151
+ id: { column: (user) => user.id },
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
+ }),
1159
+ });
1160
+ ```
1161
+
1162
+ Two variants of one table selected for the same row have their type-level selections merged into a
1163
+ single query, which can fail if they disagree. See
1164
+ [Conflicting selections between variants](https://pothos-graphql.dev/docs/plugins/drizzle/query-planning#conflicting-selections-between-variants).
1165
+
1166
+ ## Interfaces
1167
+
1168
+ ### Interfaces
1169
+
1170
+ `builder.drizzleInterface` works just like `builder.drizzleObject`, and can be used to define
1171
+ either the primary type or a variant of a table as an interface that other variants implement. The
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.
1175
+
1176
+ ```ts
1177
+ export const Viewer = builder.drizzleInterface('users', {
1178
+ variant: 'Viewer',
1179
+ select: { columns: { id: true, role: true } },
1180
+ resolveType: (user) => (user.role === 'admin' ? 'AdminViewer' : 'MemberViewer'),
1181
+ fields: (t) => ({
1182
+ id: t.exposeID('id'),
1183
+ user: t.variant('users'),
1184
+ }),
1185
+ });
1186
+
1187
+ builder.drizzleObject('users', {
1188
+ variant: 'AdminViewer',
1189
+ interfaces: [Viewer],
1190
+ select: { columns: { permissions: true } },
1191
+ fields: (t) => ({
1192
+ permissions: t.exposeStringList('permissions'),
1193
+ }),
1194
+ });
1195
+
1196
+ builder.drizzleObject('users', {
1197
+ variant: 'MemberViewer',
1198
+ interfaces: [Viewer],
1199
+ select: { columns: { id: true } },
1200
+ });
1201
+ ```
1202
+
1203
+ Fields can be added to an interface later with `builder.drizzleInterfaceField` and
1204
+ `builder.drizzleInterfaceFields`, which take the interface ref (or the table name) like their
1205
+ `drizzleObjectField(s)` counterparts:
1206
+
1207
+ ```ts
1208
+ builder.drizzleInterfaceFields(Viewer, (t) => ({
1209
+ posts: t.relatedConnection('posts'),
1210
+ }));
1211
+ ```
1212
+
1213
+ An object type implementing a drizzle interface must be based on the same table. A plain object
1214
+ type that implements one is planned with the interface's table, so fragments on it will select the
1215
+ relations it inherits.
1216
+
1217
+ ## Query planning
1218
+
1219
+ This page describes how the plugin turns a GraphQL query into drizzle queries, which is worth
1220
+ knowing when a schema issues more queries than you expect.
1221
+
1222
+ ### How fields get their data
1223
+
1224
+ A field either reads its data from a row that has already been loaded, or runs a query of its own.
1225
+
1226
+ A field's `select` is planned into the query of the nearest ancestor that runs one: a
1227
+ `t.drizzleField`, a `t.relation`, a connection, or a [fallback query](https://pothos-graphql.dev/docs/plugins/drizzle/relations#fallback-queries). The field
1228
+ then reads what it needs off the loaded row, without a query of its own. A field runs its own query
1229
+ when its `resolve` queries drizzle directly, and when the plugin issues a fallback query for a
1230
+ relation that is missing from the row.
1231
+
1232
+ A field can do both. A `t.drizzleField`, or any other field with a `select`, nested under one of
1233
+ those ancestors has its `select` planned into the parent's row, and still runs its own query when
1234
+ it resolves.
1235
+
1236
+ A field-level `select` is merged into the same query as its siblings and the type-level selection,
1237
+ rather than being kept separate for that field. Two selections of the same relation share a place
1238
+ in that query only when their arguments (`where`, `orderBy`, `limit`, ...) match. When they differ,
1239
+ the first one planned wins, and the other is loaded with a query of its own. A type-level `select`
1240
+ is planned before any field's selection, no matter where they appear in the document, and fields
1241
+ are planned in the order they are selected.
1242
+
1243
+ ### Nested selections
1244
+
1245
+ A `select` function receives `nestedSelection`, which plans the selection beneath the field for the
1246
+ field's own type. It returns the query config for that table, or a `many` config for a list field.
1247
+ Any keys you pass in are kept as they were given, so `columns` passed to `nestedSelection` will
1248
+ still narrow the parent shape:
1249
+
1250
+ ```ts
1251
+ builder.drizzleObject('users', {
1252
+ name: 'User',
1253
+ fields: (t) => ({
1254
+ previewPosts: t.field({
1255
+ type: [Post],
1256
+ select: (args, ctx, nestedSelection) => ({
1257
+ // what the query selects on the posts, limited to one
1258
+ with: { posts: nestedSelection({ limit: 1 }) },
1259
+ }),
1260
+ resolve: (user) => user.posts,
1261
+ }),
1262
+ }),
1263
+ });
1264
+ ```
1265
+
1266
+ `nestedSelection(query, path, type)` takes two more arguments:
1267
+
1268
+ - `path`: a list of field names, for selecting a field nested under the field's type. A segment can
1269
+ also be written as `{ name, type }` to pin the implementation the field must be found under.
1270
+ - `type`: a member of an interface or union, to read the selection as that type.
1271
+
1272
+ The `nestedSelection` function also carries the `path` and `segments` of the field being planned,
1273
+ which are described under [Relation queries](https://pothos-graphql.dev/docs/plugins/drizzle/relations#relation-queries).
1274
+
1275
+ ### Async selections
1276
+
1277
+ Selections are synchronous unless the schema opts in with `AsyncSelections: true`. This example
1278
+ uses request context methods that asynchronously return a user ID and a preview limit:
1279
+
1280
+ ```ts
1281
+ const builder = new SchemaBuilder<{
1282
+ DrizzleRelations: typeof relations;
1283
+ AsyncSelections: true;
1284
+ Context: {
1285
+ currentUserId: () => Promise<number>;
1286
+ previewSize: () => Promise<number>;
1287
+ };
1288
+ }>({
1289
+ plugins: [DrizzlePlugin],
1290
+ drizzle: {
1291
+ client: db,
1292
+ getTableConfig,
1293
+ relations,
1294
+ },
1295
+ });
1296
+ ```
1297
+
1298
+ With the opt-in, `select` functions, relation `query` callbacks, `relatedCount` `where` callbacks,
1299
+ and the `select` and `query` callbacks of `drizzleConnectionHelpers` may be async. Without it they
1300
+ are typed as synchronous, and an async callback is a type error.
1301
+
1302
+ The plugin still builds a single query. It waits for the callbacks, and merges what they return
1303
+ after every synchronous selection, in document order. `t.relation`, `t.relatedCount`,
1304
+ `t.drizzleField`, `t.drizzleConnection` and `t.relatedConnection` settle their plan before the
1305
+ resolver runs, and need no changes.
1306
+
1307
+ ```ts
1308
+ builder.drizzleObject('users', {
1309
+ name: 'User',
1310
+ fields: (t) => ({
1311
+ posts: t.relation('posts', {
1312
+ query: async (args, ctx) => ({ where: { authorId: await ctx.currentUserId() } }),
1313
+ }),
1314
+ previewPosts: t.field({
1315
+ type: [Post],
1316
+ select: async (args, ctx, nestedSelection) => ({
1317
+ with: { posts: await nestedSelection({ limit: await ctx.previewSize() }) },
1318
+ }),
1319
+ resolve: (user) => user.posts,
1320
+ }),
1321
+ }),
1322
+ });
1323
+ ```
1324
+
1325
+ `await` what `nestedSelection` returns before putting it in the selection, and the same for the
1326
+ `nestedQuery` passed as the fourth argument of a `t.relatedField` `select`. A selection that
1327
+ contains the promise itself will throw, and so will a `select` that returns while a nested
1328
+ selection it started is still pending. Calling `nestedSelection` and discarding a synchronous
1329
+ result is not detected, and the nested selection will not be loaded with the parent.
1330
+
1331
+ Pass `awaitSelections: true` to `drizzleConnectionHelpers(...).getQuery`, and `await` the query it
1332
+ returns. Without it, an async selection beneath the field throws, and whether there is one
1333
+ depends on the incoming document rather than on the callback you wrote. A connection helper also
1334
+ throws when its own `select` or `query` is async, whatever the document asked for:
1335
+
1336
+ ```ts
1337
+ select: async (args, ctx, nestedSelection) => ({
1338
+ with: {
1339
+ comments: await commentConnectionHelpers.getQuery(args, ctx, nestedSelection, {
1340
+ awaitSelections: true,
1341
+ }),
1342
+ },
1343
+ }),
1344
+ ```
1345
+
1346
+ `awaitSelections` is a per-call option, and is available whether or not the schema sets
1347
+ `AsyncSelections`.
1348
+
1349
+ When `AsyncSelections: true`, also `await` a connection helper's `resolve()` before inspecting or
1350
+ spreading its result: an async `query` callback makes resolution asynchronous. Returning its
1351
+ result directly from a GraphQL resolver remains supported.
1352
+
1353
+ ### Conflicting selections between variants
1354
+
1355
+ When a query selects two variants of one table for the same row, either with a fragment on each
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.