@pothos/plugin-drizzle 0.18.1 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +357 -0
- package/README.md +1199 -563
- package/dts/connection-helpers.d.ts +18 -8
- package/dts/connection-helpers.d.ts.map +1 -1
- package/dts/drizzle-field-builder.d.ts +4 -11
- package/dts/drizzle-field-builder.d.ts.map +1 -1
- package/dts/global-types.d.ts.map +1 -1
- package/dts/index.d.ts.map +1 -1
- package/dts/model-loader.d.ts +18 -17
- package/dts/model-loader.d.ts.map +1 -1
- package/dts/types.d.ts +82 -22
- package/dts/types.d.ts.map +1 -1
- package/dts/utils/adapter.d.ts +39 -0
- package/dts/utils/adapter.d.ts.map +1 -0
- package/dts/utils/await-selections.d.ts +12 -0
- package/dts/utils/await-selections.d.ts.map +1 -0
- package/dts/utils/config.d.ts +23 -3
- package/dts/utils/config.d.ts.map +1 -1
- package/dts/utils/cursors.d.ts +6 -6
- package/dts/utils/cursors.d.ts.map +1 -1
- package/dts/utils/map-query.d.ts +22 -21
- package/dts/utils/map-query.d.ts.map +1 -1
- package/dts/utils/path-info.d.ts +13 -0
- package/dts/utils/path-info.d.ts.map +1 -0
- package/dts/utils/relation-filter.d.ts +48 -0
- package/dts/utils/relation-filter.d.ts.map +1 -0
- package/dts/utils/selections.d.ts +2 -21
- package/dts/utils/selections.d.ts.map +1 -1
- package/esm/connection-helpers.d.ts +18 -8
- package/esm/connection-helpers.d.ts.map +1 -1
- package/esm/connection-helpers.js +52 -19
- package/esm/connection-helpers.js.map +1 -1
- package/esm/drizzle-field-builder.d.ts +4 -11
- package/esm/drizzle-field-builder.d.ts.map +1 -1
- package/esm/drizzle-field-builder.js +138 -96
- package/esm/drizzle-field-builder.js.map +1 -1
- package/esm/field-builder.js +61 -47
- package/esm/field-builder.js.map +1 -1
- package/esm/global-types.d.ts.map +1 -1
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +37 -61
- package/esm/index.js.map +1 -1
- package/esm/model-loader.d.ts +18 -17
- package/esm/model-loader.d.ts.map +1 -1
- package/esm/model-loader.js +76 -45
- package/esm/model-loader.js.map +1 -1
- package/esm/schema-builder.js +40 -149
- package/esm/schema-builder.js.map +1 -1
- package/esm/types.d.ts +82 -22
- package/esm/types.d.ts.map +1 -1
- package/esm/types.js.map +1 -1
- package/esm/utils/adapter.d.ts +39 -0
- package/esm/utils/adapter.d.ts.map +1 -0
- package/esm/utils/adapter.js +137 -0
- package/esm/utils/adapter.js.map +1 -0
- package/esm/utils/await-selections.d.ts +12 -0
- package/esm/utils/await-selections.d.ts.map +1 -0
- package/esm/utils/await-selections.js +19 -0
- package/esm/utils/await-selections.js.map +1 -0
- package/esm/utils/config.d.ts +23 -3
- package/esm/utils/config.d.ts.map +1 -1
- package/esm/utils/config.js +1 -4
- package/esm/utils/config.js.map +1 -1
- package/esm/utils/cursors.d.ts +6 -6
- package/esm/utils/cursors.d.ts.map +1 -1
- package/esm/utils/cursors.js +75 -92
- package/esm/utils/cursors.js.map +1 -1
- package/esm/utils/map-query.d.ts +22 -21
- package/esm/utils/map-query.d.ts.map +1 -1
- package/esm/utils/map-query.js +40 -354
- package/esm/utils/map-query.js.map +1 -1
- package/esm/utils/path-info.d.ts +13 -0
- package/esm/utils/path-info.d.ts.map +1 -0
- package/esm/utils/path-info.js +37 -0
- package/esm/utils/path-info.js.map +1 -0
- package/esm/utils/relation-filter.d.ts +48 -0
- package/esm/utils/relation-filter.d.ts.map +1 -0
- package/esm/utils/relation-filter.js +92 -0
- package/esm/utils/relation-filter.js.map +1 -0
- package/esm/utils/selections.d.ts +2 -21
- package/esm/utils/selections.d.ts.map +1 -1
- package/esm/utils/selections.js +1 -133
- package/esm/utils/selections.js.map +1 -1
- package/lib/connection-helpers.js +51 -18
- package/lib/connection-helpers.js.map +1 -1
- package/lib/drizzle-field-builder.js +136 -94
- package/lib/drizzle-field-builder.js.map +1 -1
- package/lib/field-builder.js +58 -44
- package/lib/field-builder.js.map +1 -1
- package/lib/index.js +36 -60
- package/lib/index.js.map +1 -1
- package/lib/model-loader.js +76 -45
- package/lib/model-loader.js.map +1 -1
- package/lib/schema-builder.js +40 -149
- package/lib/schema-builder.js.map +1 -1
- package/lib/types.js.map +1 -1
- package/lib/utils/adapter.js +147 -0
- package/lib/utils/adapter.js.map +1 -0
- package/lib/utils/await-selections.js +20 -0
- package/lib/utils/await-selections.js.map +1 -0
- package/lib/utils/config.js +1 -4
- package/lib/utils/config.js.map +1 -1
- package/lib/utils/cursors.js +67 -90
- package/lib/utils/cursors.js.map +1 -1
- package/lib/utils/map-query.js +28 -359
- package/lib/utils/map-query.js.map +1 -1
- package/lib/utils/path-info.js +39 -0
- package/lib/utils/path-info.js.map +1 -0
- package/lib/utils/relation-filter.js +91 -0
- package/lib/utils/relation-filter.js.map +1 -0
- package/lib/utils/selections.js +3 -158
- package/lib/utils/selections.js.map +1 -1
- package/package.json +10 -8
- package/src/connection-helpers.ts +168 -54
- package/src/drizzle-field-builder.ts +296 -176
- package/src/field-builder.ts +110 -77
- package/src/global-types.ts +0 -32
- package/src/index.ts +42 -68
- package/src/model-loader.ts +110 -71
- package/src/schema-builder.ts +76 -169
- package/src/types.ts +178 -46
- package/src/utils/adapter.ts +197 -0
- package/src/utils/await-selections.ts +31 -0
- package/src/utils/config.ts +1 -5
- package/src/utils/cursors.ts +84 -83
- package/src/utils/map-query.ts +51 -734
- package/src/utils/path-info.ts +40 -0
- package/src/utils/relation-filter.ts +185 -0
- package/src/utils/selections.ts +2 -207
- package/dts/utils/deep-equal.d.ts +0 -2
- package/dts/utils/deep-equal.d.ts.map +0 -1
- package/dts/utils/drizzle-graphql.d.ts +0 -2
- package/dts/utils/drizzle-graphql.d.ts.map +0 -1
- package/dts/utils/loader-map.d.ts +0 -11
- package/dts/utils/loader-map.d.ts.map +0 -1
- package/dts/utils/usage.d.ts +0 -5
- package/dts/utils/usage.d.ts.map +0 -1
- package/esm/utils/deep-equal.d.ts +0 -2
- package/esm/utils/deep-equal.d.ts.map +0 -1
- package/esm/utils/deep-equal.js +0 -45
- package/esm/utils/deep-equal.js.map +0 -1
- package/esm/utils/drizzle-graphql.d.ts +0 -2
- package/esm/utils/drizzle-graphql.d.ts.map +0 -1
- package/esm/utils/drizzle-graphql.js +0 -232
- package/esm/utils/drizzle-graphql.js.map +0 -1
- package/esm/utils/loader-map.d.ts +0 -11
- package/esm/utils/loader-map.d.ts.map +0 -1
- package/esm/utils/loader-map.js +0 -35
- package/esm/utils/loader-map.js.map +0 -1
- package/esm/utils/usage.d.ts +0 -5
- package/esm/utils/usage.d.ts.map +0 -1
- package/esm/utils/usage.js +0 -59
- package/esm/utils/usage.js.map +0 -1
- package/lib/utils/deep-equal.js +0 -55
- package/lib/utils/deep-equal.js.map +0 -1
- package/lib/utils/drizzle-graphql.js +0 -236
- package/lib/utils/drizzle-graphql.js.map +0 -1
- package/lib/utils/loader-map.js +0 -56
- package/lib/utils/loader-map.js.map +0 -1
- package/lib/utils/usage.js +0 -83
- package/lib/utils/usage.js.map +0 -1
- package/src/utils/deep-equal.ts +0 -57
- package/src/utils/drizzle-graphql.ts +0 -274
- package/src/utils/loader-map.ts +0 -49
- package/src/utils/usage.ts +0 -62
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type CompatibleTypes,
|
|
3
|
+
completeValue,
|
|
3
4
|
type ExposeNullability,
|
|
4
5
|
type FieldKind,
|
|
5
6
|
type FieldRef,
|
|
6
7
|
type InferredFieldOptionKeys,
|
|
7
8
|
type InputFieldMap,
|
|
8
|
-
type InputShapeFromFields,
|
|
9
9
|
type InterfaceParam,
|
|
10
10
|
isThenable,
|
|
11
11
|
type MaybePromise,
|
|
@@ -18,25 +18,25 @@ import {
|
|
|
18
18
|
type ShapeFromTypeParam,
|
|
19
19
|
type TypeParam,
|
|
20
20
|
} from '@pothos/core';
|
|
21
|
+
import {
|
|
22
|
+
getLoaderMapping,
|
|
23
|
+
type SelectedFieldNode,
|
|
24
|
+
selectedFieldNames,
|
|
25
|
+
} from '@pothos/selection-mapper';
|
|
21
26
|
import {
|
|
22
27
|
and,
|
|
23
28
|
type BuildQueryResult,
|
|
24
29
|
type DBQueryConfig,
|
|
25
|
-
eq,
|
|
26
30
|
type InferSelectModel,
|
|
27
31
|
Many,
|
|
32
|
+
type Relation,
|
|
33
|
+
relationsFilterToSQL,
|
|
28
34
|
type SQL,
|
|
29
35
|
type Table,
|
|
30
36
|
type TableRelationalConfig,
|
|
37
|
+
type TablesRelationalConfig,
|
|
31
38
|
} from 'drizzle-orm';
|
|
32
|
-
import {
|
|
33
|
-
type FieldNode,
|
|
34
|
-
Kind as GraphQLKind,
|
|
35
|
-
type GraphQLResolveInfo,
|
|
36
|
-
getNamedType,
|
|
37
|
-
isInterfaceType,
|
|
38
|
-
isObjectType,
|
|
39
|
-
} from 'graphql';
|
|
39
|
+
import type { GraphQLResolveInfo } from 'graphql';
|
|
40
40
|
import type { DrizzleRef } from './interface-ref.js';
|
|
41
41
|
import type {
|
|
42
42
|
DrizzleConnectionShape,
|
|
@@ -45,6 +45,7 @@ import type {
|
|
|
45
45
|
RelatedConnectionOptions,
|
|
46
46
|
RelatedCountOptions,
|
|
47
47
|
RelatedFieldOptions,
|
|
48
|
+
RelatedSelectionFieldOptions,
|
|
48
49
|
ShapeFromConnection,
|
|
49
50
|
TypesForRelation,
|
|
50
51
|
VariantFieldOptions,
|
|
@@ -56,8 +57,10 @@ import {
|
|
|
56
57
|
getCursorFormatter,
|
|
57
58
|
wrapConnectionResult,
|
|
58
59
|
} from './utils/cursors.js';
|
|
60
|
+
import { pathInfoFor } from './utils/path-info.js';
|
|
59
61
|
import { getRefFromModel } from './utils/refs.js';
|
|
60
|
-
import {
|
|
62
|
+
import { buildRelationFilter, type RelationQueryBuilder } from './utils/relation-filter.js';
|
|
63
|
+
import type { SelectionMap } from './utils/selections.js';
|
|
61
64
|
|
|
62
65
|
// Workaround for FieldKind not being extended on Builder classes
|
|
63
66
|
const RootBuilder: {
|
|
@@ -68,6 +71,15 @@ const RootBuilder: {
|
|
|
68
71
|
): PothosSchemaTypes.RootFieldBuilder<Types, Shape, Kind>;
|
|
69
72
|
} = RootFieldBuilder as never;
|
|
70
73
|
|
|
74
|
+
// drizzle-orm declares two parameters, but the implementation also takes the table's relations
|
|
75
|
+
// and the full relational config, which lets a `where` filter on related tables.
|
|
76
|
+
type RelationsFilterToSQL = (
|
|
77
|
+
table: Table,
|
|
78
|
+
filter: unknown,
|
|
79
|
+
tableRelations?: TableRelationalConfig['relations'],
|
|
80
|
+
tablesRelations?: TablesRelationalConfig,
|
|
81
|
+
) => SQL | undefined;
|
|
82
|
+
|
|
71
83
|
export class DrizzleObjectFieldBuilder<
|
|
72
84
|
Types extends SchemaTypes,
|
|
73
85
|
TableConfig extends TableRelationalConfig,
|
|
@@ -175,7 +187,6 @@ export class DrizzleObjectFieldBuilder<
|
|
|
175
187
|
maxSize = this.builder.options.drizzle?.maxConnectionSize,
|
|
176
188
|
defaultSize = this.builder.options.drizzle?.defaultConnectionSize,
|
|
177
189
|
query,
|
|
178
|
-
resolve: _,
|
|
179
190
|
extensions,
|
|
180
191
|
description,
|
|
181
192
|
totalCount,
|
|
@@ -188,13 +199,6 @@ export class DrizzleObjectFieldBuilder<
|
|
|
188
199
|
description?: string;
|
|
189
200
|
query?: ((args: {}, ctx: {}) => {}) | {};
|
|
190
201
|
totalCount?: boolean;
|
|
191
|
-
resolve?: (
|
|
192
|
-
query: {},
|
|
193
|
-
parent: unknown,
|
|
194
|
-
args: {},
|
|
195
|
-
ctx: {},
|
|
196
|
-
info: {},
|
|
197
|
-
) => MaybePromise<readonly {}[]>;
|
|
198
202
|
} = {},
|
|
199
203
|
connectionOptions = {},
|
|
200
204
|
edgeOptions = {},
|
|
@@ -212,34 +216,72 @@ export class DrizzleObjectFieldBuilder<
|
|
|
212
216
|
const ref = options.type ?? getRefFromModel(relationField.targetTableName, this.builder);
|
|
213
217
|
let typeName: string | undefined;
|
|
214
218
|
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
219
|
+
const filterTotalCount = this.builder.options.drizzle?.filterConnectionTotalCount !== false;
|
|
220
|
+
|
|
221
|
+
// The count for `totalCount` matches the connection's own filter: what the relation selects
|
|
222
|
+
// (including a `through` join and the relation's own `where`) plus the `where` from the
|
|
223
|
+
// field's `query`, so the count agrees with the rows being paginated. The `where` goes
|
|
224
|
+
// through the same `relationsFilterToSQL` the relational query builder puts it through, and
|
|
225
|
+
// `countRows` combines the two in the same order, so the count's predicate is the page
|
|
226
|
+
// query's predicate less the limit and the keyset clauses.
|
|
227
|
+
const buildCount = (
|
|
228
|
+
client: RelationQueryBuilder,
|
|
229
|
+
parentTable: TableConfig['table'],
|
|
230
|
+
where?: unknown,
|
|
231
|
+
): SQL<number> => {
|
|
232
|
+
const { countRows } = buildRelationFilter(
|
|
233
|
+
client,
|
|
234
|
+
relationField as Relation,
|
|
235
|
+
parentTable as Table,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
if (!where || !filterTotalCount) {
|
|
239
|
+
return countRows();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return countRows(
|
|
243
|
+
(relationsFilterToSQL as RelationsFilterToSQL)(
|
|
244
|
+
relatedTable.table as Table,
|
|
245
|
+
where,
|
|
246
|
+
relatedTable.relations,
|
|
247
|
+
schemaConfig.relations,
|
|
220
248
|
),
|
|
221
|
-
)
|
|
249
|
+
);
|
|
222
250
|
};
|
|
223
251
|
|
|
252
|
+
interface ConnectionFieldQuery {
|
|
253
|
+
limit?: number;
|
|
254
|
+
orderBy?: unknown;
|
|
255
|
+
where?: SQL;
|
|
256
|
+
columns?: Record<string, boolean>;
|
|
257
|
+
extras?: DrizzleCursorConnectionQueryOptions['extras'];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// The field's `query` may be async, so the result is a promise when it is. The `PathInfo` is
|
|
261
|
+
// provided by the adapter, and only read for a callback that takes one.
|
|
262
|
+
const resolveFieldQuery = (
|
|
263
|
+
args: PothosSchemaTypes.DefaultConnectionArguments,
|
|
264
|
+
ctx: {},
|
|
265
|
+
pathInfo?: PathInfo,
|
|
266
|
+
): MaybePromise<ConnectionFieldQuery> =>
|
|
267
|
+
completeValue(
|
|
268
|
+
(typeof query === 'function'
|
|
269
|
+
? (
|
|
270
|
+
query as (
|
|
271
|
+
args: {},
|
|
272
|
+
ctx: {},
|
|
273
|
+
pathInfo?: PathInfo,
|
|
274
|
+
) => MaybePromise<{} | null | undefined>
|
|
275
|
+
)(args, ctx, pathInfo)
|
|
276
|
+
: query) as MaybePromise<ConnectionFieldQuery | null | undefined>,
|
|
277
|
+
orEmpty,
|
|
278
|
+
);
|
|
279
|
+
|
|
224
280
|
const getQuery = (
|
|
225
281
|
args: PothosSchemaTypes.DefaultConnectionArguments,
|
|
226
282
|
ctx: {},
|
|
227
|
-
|
|
283
|
+
{ limit, orderBy, where, ...fieldQuery }: ConnectionFieldQuery,
|
|
228
284
|
) => {
|
|
229
|
-
const { limit, orderBy, where, ...fieldQuery } = ((typeof query === 'function'
|
|
230
|
-
? (query as (args: {}, ctx: {}, pathInfo?: import('./types').PathInfo) => {})(
|
|
231
|
-
args,
|
|
232
|
-
ctx,
|
|
233
|
-
pathInfo,
|
|
234
|
-
)
|
|
235
|
-
: query) ?? {}) as {
|
|
236
|
-
limit?: number;
|
|
237
|
-
orderBy?: unknown;
|
|
238
|
-
where?: SQL;
|
|
239
|
-
columns?: Record<string, boolean>;
|
|
240
|
-
extras?: DrizzleCursorConnectionQueryOptions['extras'];
|
|
241
|
-
};
|
|
242
|
-
|
|
243
285
|
const { cursorFields, columns, ...connectionQuery } = drizzleCursorConnectionQuery({
|
|
244
286
|
ctx,
|
|
245
287
|
maxSize,
|
|
@@ -268,26 +310,34 @@ export class DrizzleObjectFieldBuilder<
|
|
|
268
310
|
};
|
|
269
311
|
};
|
|
270
312
|
|
|
271
|
-
const
|
|
272
|
-
|
|
313
|
+
const countKey = `_${name as string}_count`;
|
|
314
|
+
|
|
315
|
+
// What the document asks of this connection, read the way the planner reads it (through
|
|
316
|
+
// fragments, directives, and a wrapping type), so the resolve side agrees with the plan. The
|
|
317
|
+
// selection is read once per request and shared by every parent row.
|
|
318
|
+
const connectionSelectionFromNames = (selected: ReadonlySet<string>) => {
|
|
319
|
+
const hasTotalCount = !!totalCount && selected.has('totalCount');
|
|
320
|
+
for (const field of selected) {
|
|
321
|
+
if (field !== 'totalCount' && field !== '__typename') {
|
|
322
|
+
return { hasTotalCount, totalCountOnly: false };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return { hasTotalCount, totalCountOnly: hasTotalCount };
|
|
326
|
+
};
|
|
327
|
+
const connectionSelection = (context: object, info: GraphQLResolveInfo) =>
|
|
328
|
+
connectionSelectionFromNames(selectedFieldNames(context, info));
|
|
329
|
+
|
|
330
|
+
// Built once per field, so a synchronous plan allocates nothing beyond the map itself.
|
|
331
|
+
const selectConnection = (
|
|
332
|
+
fieldQuery: ConnectionFieldQuery,
|
|
333
|
+
nested: SelectionMap | undefined,
|
|
273
334
|
context: object,
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
pathInfo: import('./types').PathInfo,
|
|
335
|
+
hasTotalCount: boolean,
|
|
336
|
+
totalCountOnly: boolean,
|
|
277
337
|
) => {
|
|
278
|
-
typeName ??= this.builder.configStore.getTypeConfig(ref).name;
|
|
279
|
-
|
|
280
|
-
const hasTotalCount = totalCount && !!getSelection(['totalCount']);
|
|
281
|
-
const hasEdges = !!getSelection(['edges']);
|
|
282
|
-
const hasNodes = !!getSelection(['nodes']);
|
|
283
|
-
const hasPageInfo = !!getSelection(['pageInfo']);
|
|
284
|
-
const totalCountOnly = hasTotalCount && !hasEdges && !hasNodes && !hasPageInfo;
|
|
285
338
|
const countSelection = {
|
|
286
|
-
[
|
|
287
|
-
getClient(this.builder, context)
|
|
288
|
-
relatedTable.table as Table,
|
|
289
|
-
buildCountFilter(parent),
|
|
290
|
-
),
|
|
339
|
+
[countKey]: (parent: TableConfig['table']) =>
|
|
340
|
+
buildCount(getClient(this.builder, context), parent, fieldQuery.where),
|
|
291
341
|
};
|
|
292
342
|
|
|
293
343
|
if (totalCountOnly) {
|
|
@@ -298,11 +348,6 @@ export class DrizzleObjectFieldBuilder<
|
|
|
298
348
|
};
|
|
299
349
|
}
|
|
300
350
|
|
|
301
|
-
const nested = nestedQuery(getQuery(args, context, pathInfo).select, {
|
|
302
|
-
getType: () => typeName!,
|
|
303
|
-
paths: [[{ name: 'nodes' }], [{ name: 'edges' }, { name: 'node' }]],
|
|
304
|
-
}) as SelectionMap;
|
|
305
|
-
|
|
306
351
|
return {
|
|
307
352
|
columns: {},
|
|
308
353
|
with: {
|
|
@@ -311,6 +356,59 @@ export class DrizzleObjectFieldBuilder<
|
|
|
311
356
|
extras: hasTotalCount ? countSelection : {},
|
|
312
357
|
};
|
|
313
358
|
};
|
|
359
|
+
|
|
360
|
+
const relationSelect = (
|
|
361
|
+
args: object,
|
|
362
|
+
context: object,
|
|
363
|
+
nestedQuery: (query: unknown, path?: unknown) => { select?: object },
|
|
364
|
+
getSelection: SelectedFieldNode,
|
|
365
|
+
pathInfo: PathInfo,
|
|
366
|
+
) => {
|
|
367
|
+
typeName ??= this.builder.configStore.getTypeConfig(ref).name;
|
|
368
|
+
|
|
369
|
+
const { hasTotalCount, totalCountOnly } = connectionSelectionFromNames(getSelection());
|
|
370
|
+
const fieldQuery = resolveFieldQuery(args, context, pathInfo);
|
|
371
|
+
// The nested plan starts now, with a query that waits for the field's `query` when that is
|
|
372
|
+
// async, so every callback beneath the connection runs in the same tick.
|
|
373
|
+
const nested = totalCountOnly
|
|
374
|
+
? undefined
|
|
375
|
+
: (nestedQuery(
|
|
376
|
+
isThenable(fieldQuery)
|
|
377
|
+
? fieldQuery.then((resolved) => getQuery(args, context, resolved).select)
|
|
378
|
+
: getQuery(args, context, fieldQuery).select,
|
|
379
|
+
{
|
|
380
|
+
getType: () => typeName!,
|
|
381
|
+
paths: [[{ name: 'nodes' }], [{ name: 'edges' }, { name: 'node' }]],
|
|
382
|
+
},
|
|
383
|
+
) as MaybePromise<SelectionMap>);
|
|
384
|
+
|
|
385
|
+
return isThenable(fieldQuery) || isThenable(nested)
|
|
386
|
+
? Promise.all([fieldQuery, nested]).then(([resolvedQuery, resolvedNested]) =>
|
|
387
|
+
selectConnection(resolvedQuery, resolvedNested, context, hasTotalCount, totalCountOnly),
|
|
388
|
+
)
|
|
389
|
+
: selectConnection(fieldQuery, nested, context, hasTotalCount, totalCountOnly);
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// The loaded path, per parent row: the rows are on the parent, only the page is needed.
|
|
393
|
+
const resolveLoaded = (
|
|
394
|
+
fieldQuery: ConnectionFieldQuery,
|
|
395
|
+
parent: unknown,
|
|
396
|
+
args: PothosSchemaTypes.DefaultConnectionArguments,
|
|
397
|
+
context: {},
|
|
398
|
+
countValue: number | undefined,
|
|
399
|
+
) => {
|
|
400
|
+
const { select, cursorFields } = getQuery(args, context, fieldQuery);
|
|
401
|
+
|
|
402
|
+
return wrapConnectionResult(
|
|
403
|
+
(parent as Record<string, unknown>)[name] as readonly {}[],
|
|
404
|
+
args,
|
|
405
|
+
select.limit,
|
|
406
|
+
getCursorFormatter(cursorFields, schemaConfig),
|
|
407
|
+
undefined,
|
|
408
|
+
parent,
|
|
409
|
+
countValue,
|
|
410
|
+
);
|
|
411
|
+
};
|
|
314
412
|
const fieldRef = (
|
|
315
413
|
this as unknown as {
|
|
316
414
|
connection: (...args: unknown[]) => FieldRef<Types, unknown>;
|
|
@@ -321,6 +419,18 @@ export class DrizzleObjectFieldBuilder<
|
|
|
321
419
|
extensions: {
|
|
322
420
|
...extensions,
|
|
323
421
|
pothosDrizzleSelect: relationSelect,
|
|
422
|
+
pothosDrizzleLoaded: (
|
|
423
|
+
value: Record<string, unknown>,
|
|
424
|
+
info: GraphQLResolveInfo,
|
|
425
|
+
context: object,
|
|
426
|
+
) => {
|
|
427
|
+
const { hasTotalCount, totalCountOnly } = connectionSelection(context, info);
|
|
428
|
+
|
|
429
|
+
return (
|
|
430
|
+
(!hasTotalCount || value[countKey] !== undefined) &&
|
|
431
|
+
(totalCountOnly || value[name as string] !== undefined)
|
|
432
|
+
);
|
|
433
|
+
},
|
|
324
434
|
},
|
|
325
435
|
description,
|
|
326
436
|
type: ref,
|
|
@@ -330,41 +440,13 @@ export class DrizzleObjectFieldBuilder<
|
|
|
330
440
|
context: {},
|
|
331
441
|
info: GraphQLResolveInfo,
|
|
332
442
|
) => {
|
|
333
|
-
const countKey = `_${name as string}_count`;
|
|
334
443
|
const parentRecord = parent as Record<string, unknown>;
|
|
335
444
|
const countValue = totalCount
|
|
336
445
|
? (parentRecord[countKey] as number | undefined)
|
|
337
446
|
: undefined;
|
|
338
447
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
parent,
|
|
342
|
-
args,
|
|
343
|
-
totalCount: countValue,
|
|
344
|
-
edges: [],
|
|
345
|
-
pageInfo: {
|
|
346
|
-
startCursor: null,
|
|
347
|
-
endCursor: null,
|
|
348
|
-
hasPreviousPage: false,
|
|
349
|
-
hasNextPage: false,
|
|
350
|
-
},
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// Detect totalCountOnly to skip cursor computation when only totalCount is requested
|
|
355
|
-
const returnType = getNamedType(info.returnType);
|
|
356
|
-
const fields =
|
|
357
|
-
isObjectType(returnType) || isInterfaceType(returnType) ? returnType.getFields() : {};
|
|
358
|
-
const totalCountOnly = info.fieldNodes.every((selection) =>
|
|
359
|
-
selection.selectionSet?.selections.every(
|
|
360
|
-
(s) =>
|
|
361
|
-
s.kind === GraphQLKind.FIELD &&
|
|
362
|
-
(fields[s.name.value]?.extensions?.pothosDrizzleTotalCount ||
|
|
363
|
-
s.name.value === '__typename'),
|
|
364
|
-
),
|
|
365
|
-
);
|
|
366
|
-
|
|
367
|
-
if (totalCountOnly) {
|
|
448
|
+
// Only totalCount was requested: the relation was never selected, so skip the cursors.
|
|
449
|
+
if (connectionSelection(context, info).totalCountOnly) {
|
|
368
450
|
return {
|
|
369
451
|
parent,
|
|
370
452
|
args,
|
|
@@ -379,17 +461,23 @@ export class DrizzleObjectFieldBuilder<
|
|
|
379
461
|
};
|
|
380
462
|
}
|
|
381
463
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
464
|
+
// The same position the select path planned this field at, recorded alongside its
|
|
465
|
+
// loader mapping, so a `query` that branches on its path pages the rows it selected.
|
|
466
|
+
// Asked of `parent`, because that is whose rows are being paged: a sibling row of the
|
|
467
|
+
// same list may have been loaded by a different plan, at a different position.
|
|
468
|
+
const position = getLoaderMapping(
|
|
469
|
+
context,
|
|
470
|
+
info.path,
|
|
471
|
+
info.parentType.name,
|
|
390
472
|
parent,
|
|
391
|
-
|
|
392
|
-
);
|
|
473
|
+
)?.position;
|
|
474
|
+
const fieldQuery = resolveFieldQuery(args, context, pathInfoFor(position));
|
|
475
|
+
|
|
476
|
+
return isThenable(fieldQuery)
|
|
477
|
+
? fieldQuery.then((resolved) =>
|
|
478
|
+
resolveLoaded(resolved, parent, args, context, countValue),
|
|
479
|
+
)
|
|
480
|
+
: resolveLoaded(fieldQuery, parent, args, context, countValue);
|
|
393
481
|
},
|
|
394
482
|
},
|
|
395
483
|
connectionOptions instanceof ObjectRef
|
|
@@ -519,31 +607,25 @@ export class DrizzleObjectFieldBuilder<
|
|
|
519
607
|
|
|
520
608
|
const { query = {}, extensions, ...rest } = options;
|
|
521
609
|
|
|
610
|
+
// Built once per field: the select allocates nothing when the nested selection is sync. The
|
|
611
|
+
// nested selection already carries the field's `query` (it is merged into the child first),
|
|
612
|
+
// so nothing is spread over it.
|
|
613
|
+
const selectRelation = (nested: unknown) => ({ columns: {}, with: { [name]: nested } });
|
|
522
614
|
const relationSelect = (
|
|
523
615
|
args: object,
|
|
524
616
|
context: object,
|
|
525
617
|
nestedQuery: (query: unknown) => {},
|
|
526
618
|
_resolveSelection: unknown,
|
|
527
619
|
pathInfo: PathInfo,
|
|
528
|
-
) =>
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
context,
|
|
538
|
-
pathInfo,
|
|
539
|
-
)
|
|
540
|
-
: query) as {}),
|
|
541
|
-
}),
|
|
542
|
-
},
|
|
543
|
-
};
|
|
544
|
-
|
|
545
|
-
return relQuery;
|
|
546
|
-
};
|
|
620
|
+
) =>
|
|
621
|
+
completeValue(
|
|
622
|
+
nestedQuery(
|
|
623
|
+
typeof query === 'function'
|
|
624
|
+
? (query as (args: {}, context: {}, pathInfo?: PathInfo) => {})(args, context, pathInfo)
|
|
625
|
+
: query,
|
|
626
|
+
),
|
|
627
|
+
selectRelation,
|
|
628
|
+
);
|
|
547
629
|
|
|
548
630
|
return this.field({
|
|
549
631
|
...(rest as {}),
|
|
@@ -551,6 +633,8 @@ export class DrizzleObjectFieldBuilder<
|
|
|
551
633
|
extensions: {
|
|
552
634
|
...extensions,
|
|
553
635
|
pothosDrizzleSelect: relationSelect as never,
|
|
636
|
+
pothosDrizzleLoaded: (value: Record<string, unknown>) =>
|
|
637
|
+
value[name as string] !== undefined,
|
|
554
638
|
},
|
|
555
639
|
resolve: (parent: Record<string, never>) => parent[name as string],
|
|
556
640
|
} as never) as never;
|
|
@@ -566,26 +650,15 @@ export class DrizzleObjectFieldBuilder<
|
|
|
566
650
|
BuildQueryResult<Types['DrizzleRelations'], TableConfig, Select & { columns: {} }>,
|
|
567
651
|
>(
|
|
568
652
|
relationName: Field,
|
|
569
|
-
options:
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
nestedQuery: (
|
|
579
|
-
query: DBQueryConfig<'many', Types['DrizzleRelations'], TableConfig>,
|
|
580
|
-
) => DBQueryConfig<'many', Types['DrizzleRelations'], TableConfig>,
|
|
581
|
-
) => Select;
|
|
582
|
-
resolve: (
|
|
583
|
-
parent: ShapeWithSelection,
|
|
584
|
-
args: Args extends InputFieldMap ? InputShapeFromFields<Args> : {},
|
|
585
|
-
ctx: Types['Context'],
|
|
586
|
-
info: unknown,
|
|
587
|
-
) => ShapeFromTypeParam<Types, Type, Nullable>;
|
|
588
|
-
},
|
|
653
|
+
options: RelatedSelectionFieldOptions<
|
|
654
|
+
Types,
|
|
655
|
+
TableConfig,
|
|
656
|
+
Type,
|
|
657
|
+
Nullable,
|
|
658
|
+
Args,
|
|
659
|
+
Select,
|
|
660
|
+
ShapeWithSelection
|
|
661
|
+
>,
|
|
589
662
|
): FieldRef<Types, ShapeFromTypeParam<Types, Type, Nullable>, 'DrizzleObject'> {
|
|
590
663
|
const schemaConfig = getSchemaConfig(this.builder);
|
|
591
664
|
const relationField = schemaConfig.relations?.[this.table].relations[relationName as string];
|
|
@@ -596,40 +669,41 @@ export class DrizzleObjectFieldBuilder<
|
|
|
596
669
|
);
|
|
597
670
|
}
|
|
598
671
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
)!;
|
|
607
|
-
};
|
|
672
|
+
// Explicit NOT NULL is required even for primary keys: SQLite permits nullable text and
|
|
673
|
+
// composite primary keys. Without a total identity the relation filter keeps EXISTS.
|
|
674
|
+
const targetKey = relationField.throughTable
|
|
675
|
+
? schemaConfig
|
|
676
|
+
.getUniqueConstraints(relationField.targetTableName)
|
|
677
|
+
.find((columns) => columns.every((column) => column.notNull))
|
|
678
|
+
: undefined;
|
|
608
679
|
|
|
609
680
|
const relationSelect = (
|
|
610
681
|
args: object,
|
|
611
682
|
context: Types['Context'],
|
|
612
683
|
nestedQuery: (query: unknown) => unknown,
|
|
613
684
|
) => {
|
|
614
|
-
const
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
685
|
+
const buildFilter = (parentTable: TableConfig['table']): SQL =>
|
|
686
|
+
buildRelationFilter(
|
|
687
|
+
getClient(this.builder, context),
|
|
688
|
+
relationField as Relation,
|
|
689
|
+
parentTable as Table,
|
|
690
|
+
targetKey,
|
|
691
|
+
).filter;
|
|
692
|
+
|
|
693
|
+
return completeValue(
|
|
694
|
+
options.select(buildFilter as never, args as never, context, nestedQuery as never) as
|
|
695
|
+
| MaybePromise<Select & { with?: unknown }>
|
|
696
|
+
| undefined,
|
|
697
|
+
pickSelection,
|
|
698
|
+
);
|
|
625
699
|
};
|
|
626
700
|
|
|
701
|
+
const { select: _select, extensions, ...fieldOptions } = options;
|
|
702
|
+
|
|
627
703
|
return this.field({
|
|
628
|
-
|
|
629
|
-
nullable: options.nullable,
|
|
630
|
-
args: options.args,
|
|
631
|
-
description: options.description,
|
|
704
|
+
...fieldOptions,
|
|
632
705
|
extensions: {
|
|
706
|
+
...extensions,
|
|
633
707
|
pothosDrizzleSelect: relationSelect as never,
|
|
634
708
|
},
|
|
635
709
|
resolve: options.resolve as never,
|
|
@@ -637,7 +711,7 @@ export class DrizzleObjectFieldBuilder<
|
|
|
637
711
|
}
|
|
638
712
|
|
|
639
713
|
relatedCount<
|
|
640
|
-
Field extends
|
|
714
|
+
Field extends ListRelation<TableConfig>,
|
|
641
715
|
Args extends InputFieldMap,
|
|
642
716
|
Where extends SQL | undefined,
|
|
643
717
|
>(
|
|
@@ -649,11 +723,47 @@ export class DrizzleObjectFieldBuilder<
|
|
|
649
723
|
const schemaConfig = getSchemaConfig(this.builder);
|
|
650
724
|
const relationField = schemaConfig.relations?.[this.table].relations[relationName as string];
|
|
651
725
|
const relatedTable = schemaConfig.relations[relationField.targetTableName];
|
|
726
|
+
// A related row counts once however many junction rows lead to it, which the count needs a
|
|
727
|
+
// column identifying a target row to say. Without one it falls back to counting the target
|
|
728
|
+
// table filtered by the relation, which says the same thing more slowly.
|
|
729
|
+
const targetKey = schemaConfig.findPrimaryKey(relationField.targetTableName);
|
|
730
|
+
const distinctBy = targetKey?.length === 1 && targetKey[0].notNull ? targetKey[0] : undefined;
|
|
731
|
+
|
|
732
|
+
// Built once per field; the `extras` function it returns is what the plan carried before.
|
|
733
|
+
const countExtras = (
|
|
734
|
+
whereClause: SQL | undefined,
|
|
735
|
+
ctx: Types['Context'],
|
|
736
|
+
buildFilter: (parent: TableConfig['table']) => SQL,
|
|
737
|
+
) =>
|
|
738
|
+
({
|
|
739
|
+
extras: {
|
|
740
|
+
[countKey]: (parent: TableConfig['table']) => {
|
|
741
|
+
const client = getClient(this.builder, ctx);
|
|
742
|
+
|
|
743
|
+
if (!distinctBy) {
|
|
744
|
+
return client.$count(
|
|
745
|
+
relatedTable.table as Table,
|
|
746
|
+
whereClause ? and(buildFilter(parent), whereClause) : buildFilter(parent),
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return buildRelationFilter(
|
|
751
|
+
client,
|
|
752
|
+
relationField as Relation,
|
|
753
|
+
parent as Table,
|
|
754
|
+
).countDistinctRows(distinctBy, whereClause);
|
|
755
|
+
},
|
|
756
|
+
},
|
|
757
|
+
}) as never;
|
|
652
758
|
|
|
653
759
|
return this.relatedField(relationName, {
|
|
654
760
|
...options,
|
|
655
761
|
type: 'Int' as never,
|
|
656
762
|
nullable: false,
|
|
763
|
+
extensions: {
|
|
764
|
+
...(options as { extensions?: Record<string, unknown> }).extensions,
|
|
765
|
+
pothosDrizzleLoaded: (value: Record<string, unknown>) => value[countKey] !== undefined,
|
|
766
|
+
},
|
|
657
767
|
select: (
|
|
658
768
|
buildFilter: (parent: TableConfig['table']) => SQL,
|
|
659
769
|
args: object,
|
|
@@ -661,18 +771,14 @@ export class DrizzleObjectFieldBuilder<
|
|
|
661
771
|
) => {
|
|
662
772
|
const whereClause =
|
|
663
773
|
typeof where === 'function'
|
|
664
|
-
? (where as (args: unknown, ctx: unknown) => SQL | undefined)(args, ctx)
|
|
774
|
+
? (where as (args: unknown, ctx: unknown) => MaybePromise<SQL | undefined>)(args, ctx)
|
|
665
775
|
: where;
|
|
666
776
|
|
|
667
|
-
return
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
whereClause ? and(buildFilter(parent), whereClause) : buildFilter(parent),
|
|
673
|
-
),
|
|
674
|
-
},
|
|
675
|
-
} as never;
|
|
777
|
+
return isThenable(whereClause)
|
|
778
|
+
? whereClause.then((resolved) =>
|
|
779
|
+
countExtras(resolved as SQL | undefined, ctx, buildFilter),
|
|
780
|
+
)
|
|
781
|
+
: countExtras(whereClause, ctx, buildFilter);
|
|
676
782
|
},
|
|
677
783
|
resolve: (parent: Record<string, number>) => parent[countKey],
|
|
678
784
|
} as never) as FieldRef<Types, number, 'DrizzleObject'>;
|
|
@@ -762,3 +868,17 @@ export class DrizzleObjectFieldBuilder<
|
|
|
762
868
|
};
|
|
763
869
|
}
|
|
764
870
|
}
|
|
871
|
+
|
|
872
|
+
/** A relation `query` callback may return nothing; the connection then adds no filter. */
|
|
873
|
+
function orEmpty<T extends object>(query: T | null | undefined): T {
|
|
874
|
+
return query ?? ({} as T);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/** A `relatedField` select's map, with the columns it may have left out defaulted. */
|
|
878
|
+
function pickSelection(selection: { columns?: {}; extras?: unknown; with?: unknown } | undefined) {
|
|
879
|
+
return {
|
|
880
|
+
columns: selection?.columns ?? {},
|
|
881
|
+
extras: selection?.extras,
|
|
882
|
+
with: selection?.with,
|
|
883
|
+
};
|
|
884
|
+
}
|