Query planning
How the plugin turns a GraphQL query into prisma queries
This page describes how the plugin turns a GraphQL query into prisma queries, which is worth knowing when a schema issues more queries than you expect.
How fields get their data
A field either reads its data from a row that has already been loaded, or runs a query of its own.
A field's select is planned into the query of the nearest ancestor that runs one: a
t.prismaField, a t.relation, a connection, or a
fallback query. The field then reads what it needs off the loaded
row, without a query of its own. A field runs its own query when its resolve queries prisma
directly, and when the plugin issues a fallback query for a t.relation that is missing from the
row.
A field can do both. A t.prismaField, or any other field with a select, nested under one of
those ancestors has its select planned into the parent's row, and still runs its own query when
it resolves.
A field-level select is merged into the same query as its siblings and the type-level selection,
rather than getting a copy of the row for that field alone. Two selections of the same relation
share a place in that query only when their arguments (where, orderBy, take, ...) match. When
they differ, the first one planned wins, and the other is loaded with a query of its own. A
type-level select or include is planned before any field's selection, no matter where they
appear in the document, and fields are planned in the order they are selected.
Async selections
Selections are synchronous unless the schema opts in with AsyncSelections: true. This example
uses request context methods that asynchronously return a user ID and a preview limit:
const builder = new SchemaBuilder<{
PrismaTypes: PrismaTypes;
AsyncSelections: true;
Context: {
currentUserId: () => Promise<number>;
previewSize: () => Promise<number>;
};
}>({
plugins: [PrismaPlugin],
prisma: {
client: prisma,
dmmf: getDatamodel(),
},
});With the opt-in, select functions, relation query callbacks, relationCount where callbacks,
and the select and query callbacks of prismaConnectionHelpers may be async. Without it they
are typed as synchronous, and an async callback is a type error.
The plugin still builds a single query. It waits for the callbacks, and merges what they return
after every synchronous selection, in document order. t.relation, t.relationCount,
t.prismaField, t.prismaConnection and t.relatedConnection settle their plan before the
resolver runs, and need no changes. The following example uses a registered Comment Prisma
object ref and its Post.comments relation:
builder.prismaObject('Post', {
fields: (t) => ({
comments: t.relation('comments', {
query: async (args, ctx) => ({ where: { authorId: await ctx.currentUserId() } }),
}),
previewComments: t.field({
type: [Comment],
select: async (args, ctx, nestedSelection) => ({
comments: await nestedSelection({ take: await ctx.previewSize() }),
}),
resolve: (post) => post.comments,
}),
}),
});await what nestedSelection returns before putting it in the selection. A selection that
contains the promise itself will throw, and so will a select that returns while a nested
selection it started is still pending. Calling nestedSelection and discarding a synchronous
result is not detected, and the nested selection will not be loaded with the parent, so the field
falls back to its own query.
Pass awaitSelections: true to queryFromInfo and prismaConnectionHelpers(...).getQuery, and
await the query they return. Without it, an async selection beneath the field throws, and
whether there is one depends on the incoming document rather than on the callback you wrote. A
connection helper also throws when its own select or query is async, whatever the document
asked for:
const post = await prisma.post.findUniqueOrThrow({
where: { id: args.id },
...(await queryFromInfo({ context, info, awaitSelections: true })),
});awaitSelections is a per-call option, and is available whether or not the schema sets
AsyncSelections.
Optimized queries without t.prismaField
In some cases, it may be useful to get an optimized query for fields where you can't use
t.prismaField.
This may be required for combining with other plugins, or because your query does not directly
return a PrismaObject. In these cases, you can use the queryFromInfo helper. An example of this
might be a mutation that wraps the prisma object in a result type.
The example assumes a Post model with id, title, and authorId columns, an existing User
record for context.userId, and the builder setup with that context type.
import type { Post as PostRow } from '../lib/prisma/client';
import { queryFromInfo } from '@pothos/plugin-prisma';
const Post = builder.prismaObject('Post', {
fields: (t) => ({
title: t.exposeString('title'),
}),
});
const CreatePostResult = builder.objectRef<{
success: boolean;
post: PostRow | null;
}>('CreatePostResult').implement({
fields: (t) => ({
success: t.exposeBoolean('success'),
post: t.field({
type: Post,
nullable: true,
resolve: (result) => result.post,
}),
}),
});
builder.mutationType({
fields: (t) => ({
createPost: t.field({
type: CreatePostResult,
args: {
title: t.arg.string({ required: true }),
},
resolve: async (parent, args, context, info) => {
if (!args.title.trim()) {
return { success: false, post: null };
}
const post = await prisma.post.create({
...(await queryFromInfo({
context,
info,
path: ['post'],
awaitSelections: true,
})),
data: {
title: args.title,
authorId: context.userId,
},
});
return { success: true, post };
},
}),
}),
});The columns and relations the query selected come back on the rows, along with anything you passed
in as select, and the rows are typed to match.
To require data even when the client does not request it, pass an initial select (or include)
to queryFromInfo. For example, add select: { id: true } beside path in the call above to
ensure the returned post includes its ID. When nothing is selected at path, the helper returns
that initial selection unchanged, or an empty query object if no initial selection was supplied.
The path is followed through fragments in the query, including inline fragments and fragment
spreads that narrow an interface or union to one of its implementations. Every selection of the
field that is found is merged into the query. If several implementations share a field name,
matches whose field returns a different Prisma model are ignored. When you need to target a
specific implementation, a segment can be written as { name, type }. The field then only matches
when it is selected directly, or under a fragment on that type or one of its subtypes:
const user = await prisma.user.findUniqueOrThrow({
where: { id: args.id },
...queryFromInfo({
context,
info,
typeName: 'User',
// only match `appointment` when selected inside `... on AppointmentEntry`
path: [{ name: 'appointment', type: 'AppointmentEntry' }],
}),
});
// user is loaded with the selections from `... on AppointmentEntry`,
// and nothing from an `appointment` field on another implementationConflicting selections between variants
When a query selects two variants of one model for the same row, either with a fragment on each
under one field, or through a t.variant field, the plugin will throw if their type-level
select/include ask for the same relation with different arguments:
PothosValidationError: Type-level selections of Viewer and Admin conflict on relation "posts".
Move the relation arguments to a field-level select on one of the types.Both variants describe one row, so their type-level selections are merged into a single query.
To fix this, keep the relation with its arguments in the select of the field that needs it, on
one of the variants. A field-level selection that conflicts with what the row already holds falls
back to a query of its own, rather than failing the request.