diff --git a/packages/plugin-prisma/src/field-builder.ts b/packages/plugin-prisma/src/field-builder.ts index ce7e5a47e..295dd499e 100644 --- a/packages/plugin-prisma/src/field-builder.ts +++ b/packages/plugin-prisma/src/field-builder.ts @@ -1,4 +1,4 @@ -import { GraphQLResolveInfo } from 'graphql'; +import { getNamedType, GraphQLResolveInfo, isInterfaceType, isObjectType, Kind } from 'graphql'; import { FieldKind, FieldRef, @@ -145,6 +145,19 @@ fieldBuilderProto.prismaConnection = function prismaConnection< withUsageCheck: !!this.builder.options.prisma?.onUnusedQuery, }); + const returnType = getNamedType(info.returnType); + const fields = + isObjectType(returnType) || isInterfaceType(returnType) ? returnType.getFields() : {}; + + const selection = info.fieldNodes[0]; + + const totalCountOnly = + selection.selectionSet?.selections.length === 1 && + selection.selectionSet.selections.every( + (s) => + s.kind === Kind.FIELD && fields[s.name.value]?.extensions?.pothosPrismaTotalCount, + ); + return resolvePrismaCursorConnection( { parent, @@ -157,13 +170,16 @@ fieldBuilderProto.prismaConnection = function prismaConnection< totalCount: totalCount && (() => totalCount(parent, args as never, context, info)), }, formatCursor, - (q) => - checkIfQueryIsUsed( + (q) => { + if (totalCountOnly) return []; + + return checkIfQueryIsUsed( this.builder, query, info, resolve(q as never, parent, args as never, context, info) as never, - ), + ); + }, ); }, }, @@ -180,6 +196,9 @@ fieldBuilderProto.prismaConnection = function prismaConnection< ) => ({ totalCount: t.int({ nullable: false, + extensions: { + pothosPrismaTotalCount: true, + }, resolve: (parent, args, context) => parent.totalCount?.(), }), ...(connectionOptions as { fields?: (t: unknown) => {} }).fields?.(t), diff --git a/packages/plugin-prisma/src/index.ts b/packages/plugin-prisma/src/index.ts index 506655a7a..b086c908a 100644 --- a/packages/plugin-prisma/src/index.ts +++ b/packages/plugin-prisma/src/index.ts @@ -1,7 +1,7 @@ import './global-types'; import './schema-builder'; import './field-builder'; -import { GraphQLFieldResolver } from 'graphql'; +import { GraphQLFieldResolver, GraphQLResolveInfo } from 'graphql'; import SchemaBuilder, { BasePlugin, BuildCache, @@ -121,7 +121,7 @@ export class PrismaPlugin extends BasePlugin { const parentConfig = this.buildCache.getTypeConfig(fieldConfig.parentType); const loadedCheck = fieldConfig.extensions?.pothosPrismaLoaded as - | ((val: unknown) => boolean) + | ((val: unknown, info: GraphQLResolveInfo) => boolean) | undefined; const loaderCache = parentConfig.extensions?.pothosPrismaLoader as ( model: unknown, @@ -154,7 +154,7 @@ export class PrismaPlugin extends BasePlugin { } } - if ((!loadedCheck || loadedCheck(parent)) && mapping) { + if ((!loadedCheck || loadedCheck(parent, info)) && mapping) { setLoaderMappings(context, info, mapping); return resolver(parent, args, context, info); diff --git a/packages/plugin-prisma/src/prisma-field-builder.ts b/packages/plugin-prisma/src/prisma-field-builder.ts index d2dd70ce4..105eea54a 100644 --- a/packages/plugin-prisma/src/prisma-field-builder.ts +++ b/packages/plugin-prisma/src/prisma-field-builder.ts @@ -1,6 +1,13 @@ /* eslint-disable no-nested-ternary */ /* eslint-disable no-underscore-dangle */ -import { FieldNode, GraphQLResolveInfo } from 'graphql'; +import { + FieldNode, + getNamedType, + GraphQLResolveInfo, + isInterfaceType, + isObjectType, + Kind, +} from 'graphql'; import { CompatibleTypes, ExposeNullability, @@ -265,17 +272,17 @@ export class PrismaObjectFieldBuilder< nestedQuery: (query: unknown, path?: unknown) => { select?: {} }, getSelection: (path: string[]) => FieldNode | null, ) => { + typeName ??= this.builder.configStore.getTypeConfig(ref).name; const nested = nestedQuery(getQuery(args, context), { - getType: () => { - if (!typeName) { - typeName = this.builder.configStore.getTypeConfig(ref).name; - } - return typeName; - }, + getType: () => typeName!, paths: [[{ name: 'nodes' }], [{ name: 'edges' }, { name: 'node' }]], }) as SelectionMap; + const selection = getSelection([])!; const hasTotalCount = totalCount && !!getSelection(['totalCount']); + + const totalCountOnly = selection.selectionSet?.selections.length === 1 && hasTotalCount; + const countSelect = this.builder.options.prisma.filterConnectionTotalCount ? nested.where ? { where: nested.where } @@ -285,7 +292,9 @@ export class PrismaObjectFieldBuilder< return { select: { ...(hasTotalCount ? { _count: { select: { [name]: countSelect } } } : {}), - [name]: nested?.select + [name]: totalCountOnly + ? undefined + : nested?.select ? { ...nested, select: { @@ -309,7 +318,22 @@ export class PrismaObjectFieldBuilder< extensions: { ...extensions, pothosPrismaSelect: relationSelect, - pothosPrismaLoaded: (value: Record) => value[name] !== undefined, + pothosPrismaLoaded: (value: Record, info: GraphQLResolveInfo) => { + const returnType = getNamedType(info.returnType); + const fields = + isObjectType(returnType) || isInterfaceType(returnType) ? returnType.getFields() : {}; + + const selection = info.fieldNodes[0]; + + const totalCountOnly = + selection.selectionSet?.selections.length === 1 && + selection.selectionSet.selections.every( + (s) => + s.kind === Kind.FIELD && fields[s.name.value]?.extensions?.pothosPrismaTotalCount, + ); + + return totalCountOnly || value[name] !== undefined; + }, pothosPrismaFallback: resolve && (( @@ -342,7 +366,7 @@ export class PrismaObjectFieldBuilder< return wrapConnectionResult( parent, - (parent as Record)[name], + (parent as Record)[name] ?? [], args, connectionQuery.take, formatCursor, @@ -360,6 +384,9 @@ export class PrismaObjectFieldBuilder< ) => ({ totalCount: t.int({ nullable: false, + extensions: { + pothosPrismaTotalCount: true, + }, resolve: (parent, args, context) => parent.totalCount, }), ...(connectionOptions as { fields?: (t: unknown) => {} }).fields?.(t), diff --git a/packages/plugin-prisma/src/util/map-query.ts b/packages/plugin-prisma/src/util/map-query.ts index 3787c98a1..0e9bd0b5b 100644 --- a/packages/plugin-prisma/src/util/map-query.ts +++ b/packages/plugin-prisma/src/util/map-query.ts @@ -347,6 +347,10 @@ function addFieldSelection( return selectionToQuery(fieldState); }, (path) => { + if (path.length === 0) { + return selection; + } + const returnType = getNamedType(field.type); let node: FieldNode | null = null; diff --git a/packages/plugin-prisma/tests/counts.test.ts b/packages/plugin-prisma/tests/counts.test.ts index 11486246c..fa36657df 100644 --- a/packages/plugin-prisma/tests/counts.test.ts +++ b/packages/plugin-prisma/tests/counts.test.ts @@ -143,16 +143,6 @@ describe('prisma counts', () => { "model": "User", "runInTransaction": false, }, - { - "action": "findMany", - "args": { - "skip": 0, - "take": 2, - }, - "dataPath": [], - "model": "User", - "runInTransaction": false, - }, { "action": "count", "args": undefined, @@ -237,6 +227,44 @@ describe('prisma counts', () => { `); }); + it('queries only totalCount on connection', async () => { + const query = gql` + query { + userConnection { + totalCount + } + } + `; + + const result = await execute({ + schema, + document: query, + contextValue: { user: { id: 1 } }, + }); + + expect(result).toMatchInlineSnapshot(` + { + "data": { + "userConnection": { + "totalCount": 100, + }, + }, + } + `); + + expect(queries).toMatchInlineSnapshot(` + [ + { + "action": "count", + "args": undefined, + "dataPath": [], + "model": "User", + "runInTransaction": false, + }, + ] + `); + }); + it('connection totalCount in fragment', async () => { const query = gql` query { @@ -550,13 +578,6 @@ describe('prisma counts', () => { "posts": true, }, }, - "posts": { - "orderBy": { - "createdAt": "desc", - }, - "skip": 0, - "take": 21, - }, "profile": { "include": { "user": {