Pothos v4 is now available! 🎉Check out the full migration guide here

Pothos

Relations

You can add fields for relations using the t.relation method:

builder.queryType({
  fields: (t) => ({
    me: t.prismaField({
      type: 'User',
      resolve: async (query, root, args, ctx, info) =>
        prisma.user.findUniqueOrThrow({
          ...query,
          where: { id: ctx.userId },
        }),
    }),
  }),
});
 
builder.prismaObject('User', {
  fields: (t) => ({
    id: t.exposeID('id'),
    email: t.exposeString('email'),
    posts: t.relation('posts'),
  }),
});
 
builder.prismaObject('Post', {
  fields: (t) => ({
    id: t.exposeID('id'),
    title: t.exposeString('title'),
    author: t.relation('author'),
  }),
});

t.relation defines a field that can be pre-loaded by a parent resolver. This will create something like { include: { author: true }} that will be passed as part of the query argument of a prismaField resolver. If the parent is another relation field, the includes will become nested, and the full relation chain will be passed to the prismaField that started the chain.

For example the query:

query {
  me {
    posts {
      author {
        id
      }
    }
  }
}

the me prismaField would receive something like the following as its query parameter:

{
  include: {
    posts: {
      include: {
        author: true;
      }
    }
  }
}

This will work perfectly for the majority of queries. There are a number of edge cases that make it impossible to resolve everything in a single query. When this happens Pothos will automatically construct an additional query to ensure that everything is still loaded correctly, and split into as few efficient queries as possible. This process is described in more detail below

Fallback queries

There are some cases where data can not be pre-loaded by a prisma field. In these cases, pothos will issue a findUnique query for the parent of any fields that were not pre-loaded, and select the missing relations so those fields can be resolved with the correct data. These queries should be very efficient, are batched by pothos to combine requirements for multiple fields into one query, and batched by Prisma to combine multiple queries (in an n+1 situation) to a single sql query.

The following are some edge cases that could cause an additional query to be necessary:

  • The parent object was not loaded through a field defined with t.prismaField, or t.relation
  • The root prismaField did not correctly spread the query arguments in is prisma call.
  • The query selects multiple fields that use the same relation with different filters, sorting, or limits
  • The query contains multiple aliases for the same relation field with different arguments in a way that results in different query options for the relation.
  • A relation field has a query that is incompatible with the default includes of the parent object

All of the above should be relatively uncommon in normal usage, but the plugin ensures that these types of edge cases are automatically handled when they do occur.

Filters, Sorting, and arguments

So far we have been describing very simple queries without any arguments, filtering, or sorting. For t.prismaField definitions, you can add arguments to your field like normal, and pass them into your prisma query as needed. For t.relation the flow is slightly different because we are not making a prisma query directly. We do this by adding a query option to our field options. Query can either be a query object, or a method that returns a query object based on the field arguments.

builder.prismaObject('User', {
  fields: (t) => ({
    id: t.exposeID('id'),
    posts: t.relation('posts', {
      // We can define arguments like any other field
      args: {
        oldestFirst: t.arg.boolean(),
      },
      // Then we can generate our query conditions based on the arguments
      query: (args, context) => ({
        orderBy: {
          createdAt: args.oldestFirst ? 'asc' : 'desc',
        },
      }),
    }),
  }),
});

The returned query object will be added to the include section of the query argument that gets passed into the first argument of the parent t.prismaField, and can include things like where, skip, take, and orderBy. The query function will be passed the arguments for the field, and the context for the current request. Because it is used for pre-loading data, and solving n+1 issues, it can not be passed the parent object because it may not be loaded yet.

builder.prismaObject('User', {
  fields: (t) => ({
    id: t.exposeID('id'),
    email: t.exposeString('email'),
    posts: t.relation('posts', {
      // We can define arguments like any other field
      args: {
        oldestFirst: t.arg.boolean(),
      },
      // Then we can generate our query conditions based on the arguments
      query: (args, context) => ({
        orderBy: {
          createdAt: args.oldestFirst ? 'asc' : 'desc',
        },
      }),
    }),
  }),
});

On this page