PluginsPrisma Next

Relay nodes

Defining Relay nodes for contract-backed models

builder.prismaNode defines a model type that implements the Relay Node interface, so clients can fetch it by its global ID with the node and nodes query fields. You'll need the Relay plugin. For pagination, see Connections.

Define a node

builder.prismaNode takes the same options as builder.prismaObject, plus an id:

builder.prismaNode('Post', {
  id: { field: 'id' },
  fields: (t) => ({
    title: t.exposeString('title'),
    author: t.relation('author'),
  }),
});

Authorize node lookups

Any client can load a node directly by its ID through node and nodes, so add auth checks on the type, such as Scope Auth's authScopes, for rows not everyone should read. Filtering lookups with collection or a per-request client isn't access control.

The ID holds the column's value, as text, or a JSON array of the values when id.field lists several columns, as the Prisma plugin's IDs do. To control the format of the global ID, pass resolve and parse:

builder.prismaNode('Comment', {
  id: {
    field: 'id',
    resolve: (comment) => `comment:${comment.id}`,
    parse: (id) => id.slice('comment:'.length),
  },
  fields: (t) => ({ body: t.exposeString('body') }),
});

prismaNode adds these options:

  • id.field: a column, or a list of columns, covering a non-null primary or unique key.
  • id.parse / id.resolve: custom ID serialization. Pass both or neither.
  • Other id options, such as description, are passed to the ID field.
  • collection: the Collection nodes load from. See Filter node lookups.

A timestamp in id.field has the same precision limit as a connection's order.

Filter node lookups

To limit which rows node and nodes can return, pass collection. It receives the context, so it can filter by the current user. Here node only finds the current user's posts:

builder.prismaNode('Post', {
  id: { field: 'id' },
  collection: (ctx) => db.orm.Post.where({ authorId: ctx.userId }),
  fields: (t) => ({ title: t.exposeString('title') }),
});

node and nodes return null for rows the filter excludes. Only filter the collection; don't call limit(), offset(), cursor() or include() on it.

collection only applies to node and nodes. Rows reached through relations and other fields don't pass through it.

Several node types for one model

If you define more than one node type for a model, such as a DraftPost variant of Post, each type has its own global IDs. When one field can return more than one of these types, return a prismaInterface with a resolveType, or brand each row with its type's addBrand:

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

const DraftPost = builder.prismaNode('Post', {
  variant: 'DraftPost',
  id: { field: 'id' },
  fields: (t) => ({ title: t.exposeString('title') }),
});

builder.queryField('latestDraft', (t) =>
  t.field({
    type: builder.nodeInterfaceRef(),
    nullable: true,
    resolve: async (_root, _args, ctx, info) => {
      const drafts = await collectionFromInfo({
        collection: db.orm.Post.where({ authorId: ctx.userId, published: 0 }).orderBy(
          (post) => post.createdAt.desc(),
        ),
        context: ctx,
        info,
        typeName: 'DraftPost',
      });
      const post = await drafts.first();
      return post && DraftPost.addBrand(post);
    },
  }),
);