PluginsPrisma Next

Objects

Defining GraphQL types backed by contract models

builder.prismaObject defines a GraphQL object type for a model in your contract.

Define a type

Pass the model name and the usual object type options. Column types come from the contract.

builder.prismaObject('User', {
  fields: (t) => ({
    id: t.exposeID('id'),
    email: t.exposeString('email'),
  }),
});

The GraphQL type is named after the model. Pass name to call it something else.

To add fields to a type elsewhere, use prismaObjectFields. To expose the same model as another GraphQL type, define a variant or a prismaInterface. To return these objects from a query field, use t.prismaField; see Queries and selections.

Selections

A field that reads columns it doesn't expose, or data from a relation, declares them with select. The plugin then loads them along with the rest of the query.

builder.prismaObject('User', {
  // Loaded whenever a User is queried
  select: { firstName: true, lastName: true },
  fields: (t) => ({
    fullName: t.string({
      resolve: (user) => `${user.firstName} ${user.lastName}`,
    }),
    // Loaded only when emailDomain is queried
    emailDomain: t.string({
      select: ['email'],
      resolve: (user) => user.email.split('@')[1] ?? '',
    }),
    publishedPostCount: t.int({
      select: {
        posts: (posts) => ({ publishedCount: posts.where({ published: 1 }).count() }),
      },
      resolve: (user) => user.publishedCount,
    }),
  }),
});

A select on the type is loaded whenever the type is queried, and every field can read it. A select on a field is loaded only when that field is queried. The primary key is always loaded.

A select accepts:

  • Columns: { email: true } or ['email'].
  • Relations: { posts: true }, which loads the relation's scalar columns.
  • Filtered or sorted relations: { posts: { where: { published: 1 }, orderBy: { createdAt: 'desc' }, limit: 5 } }. where also takes the ORM's predicate callback, and orderBy its selectors, as in t.relation's query.
  • A select of the relation's rows, in the same form: { comments: { limit: 5, select: { post: true } } }.
  • type, a ref or model name of a variant of the relation's target, which loads only that variant's rows: { tasks: { type: Bug } }.
  • In a field's select only, counts and aggregates of a to-many relation: { posts: (posts) => ({ postCount: posts.count() }) }. Each key (postCount) is added to the row, so don't reuse a column or relation name.

A to-one relation takes only type and select. Read aggregates in a resolver; t.expose* only works on columns.

The type-level selects of one model, including its variants and interfaces, must load each relation they name the same way, at any depth, or the schema build throws. A where or orderBy function only matches itself, so share it between them. Load another version of a relation with t.relation or a field's select.

To filter a to-one relation, select it and return null from the resolver. Here a comment shows its post's title only when the post is published:

builder.prismaObject('Comment', {
  fields: (t) => ({
    postTitle: t.string({
      nullable: true,
      select: { post: true },
      resolve: (comment) => (comment.post.published ? comment.post.title : null),
    }),
  }),
});

Selecting per request

A field's select can also be a function of its arguments and context. Here the field selects lastName only when the client asks for the full name. A column that only some branches select is optional on the parent:

name: t.string({
  args: { full: t.arg.boolean() },
  select: (args) => (args.full ? { firstName: true, lastName: true } : { firstName: true }),
  resolve: (user, args) =>
    args.full && user.lastName ? `${user.firstName} ${user.lastName}` : user.firstName,
}),

With AsyncSelections: true in the builder's types, the function can be async.

Its third argument, nestedSelection, includes the dependencies and relations requested beneath a custom field returning objects. Without an explicit select, it loads scalar columns alongside those GraphQL requirements. An explicit select can start from a narrower set. See Indirect relations for examples.

select, query and where callbacks run while planning or building a query, rather than for each returned row. They can run more than once for reloads or aggregate planning, so keep them free of side effects.

Resolver data

A resolver can read the model's primary or non-null unique key, the type's shared select, and its own field's select. Other columns aren't guaranteed to be loaded.

Rows the plugin loads carry a type-only marker, such as PrismaPlanned<'User'>. For returning rows from custom resolvers, see Returning rows from other fields.

Auth checks

Auth checks on the type, and other plugins' field options such as authScopes and grantScopes, see:

  • the primary key
  • the columns and relations in the selects of the type and its interfaces
  • on a t.expose* field, the exposed column

Only resolve can read what a field's own select loads. If a row is missing any of this, such as a row of a variant model with its own table, the plugin reloads it by its key before the check.

Mutations

Use resolve for a field that writes data. It receives the model's Collection prepared for the GraphQL query. Filter it to the row you want to change, then call Prisma's write method:

builder.mutationType({
  fields: (t) => ({
    renameUser: t.prismaField({
      type: 'User',
      nullable: true,
      args: {
        id: t.arg.id({ required: true }),
        firstName: t.arg.string({ required: true }),
      },
      resolve: (users, _root, args) =>
        users.where({ id: String(args.id) }).update({ firstName: args.firstName }),
    }),
  }),
});

The updated row becomes the mutation result, with the fields requested by the client.

Return a payload

For a result shaped like { user: ... }, use collectionFromInfo to load the fields requested on user. The path: ['user'] option tells it where that object appears in the result:

import { collectionFromInfo } from '@pothos/plugin-prisma-next';

const RenameUserPayload = builder
  .objectRef<{ user: typeof User.$inferType | null }>('RenameUserPayload')
  .implement({
    fields: (t) => ({ user: t.expose('user', { type: User, nullable: true }) }),
  });

builder.mutationField('renameUser', (t) =>
  t.field({
    type: RenameUserPayload,
    args: { id: t.arg.id({ required: true }), firstName: t.arg.string({ required: true }) },
    resolve: async (_root, args, ctx, info) => {
      const users = await collectionFromInfo({
        collection: db.orm.User.where({ id: String(args.id) }),
        context: ctx,
        info,
        path: ['user'],
      });
      return { user: await users.update({ firstName: args.firstName }) };
    },
  }),
);

If the payload field is an interface or union, pass typeName to choose the GraphQL object type to load. For mutations on inherited models, see Model inheritance.

An exported function that returns rows from collectionFromInfo needs a return type, such as Promise<PrismaPlanned<'User'> | null>, for TypeScript to emit its declaration.

See Transactions to run the write and the reads in one transaction.

prismaFieldWithInput

With the with-input plugin, t.prismaFieldWithInput combines t.prismaField with t.fieldWithInput, and takes collection, query or resolve the same way. The input fields become an input object argument:

builder.mutationField('updateFirstName', (t) =>
  t.prismaFieldWithInput({
    type: 'User',
    nullable: true,
    input: {
      id: t.input.id({ required: true }),
      firstName: t.input.string({ required: true }),
    },
    resolve: (users, _root, args) =>
      users.where({ id: String(args.input.id) }).update({ firstName: args.input.firstName }),
  }),
);

Adding fields

To add fields to a type from another file, use prismaObjectField or prismaObjectFields. builder.objectField won't work here because it doesn't support select.

builder.prismaObjectField('User', 'displayName', (t) =>
  t.string({
    select: ['firstName', 'lastName'],
    resolve: (user) => `${user.firstName} ${user.lastName}`,
  }),
);

If the resolver reads the type's select, pass the ref that prismaObject returns instead of the model name. Passing a variant's ref adds fields to that variant.

const User = builder.prismaObject('User', {
  select: { firstName: true, lastName: true },
  fields: (t) => ({ id: t.exposeID('id') }),
});

builder.prismaObjectField(User, 'initials', (t) =>
  t.string({
    resolve: (user) => `${user.firstName[0]}${user.lastName[0]}`,
  }),
);