Pothos

Indirect relations

Selecting fields from a nested GraphQL field

By default, the nestedSelection function will return selections based on the type of the current field. nestedSelection can also be used to get a selection from a field nested deeper inside other fields. This is useful if the field returns a type that is not a prismaObject, but a field nested inside the returned type is.

const PostRef = builder.prismaObject('Post', {
  fields: (t) => ({
    title: t.exposeString('title'),
    content: t.exposeString('content'),
    author: t.relation('author'),
  }),
});

const PostPreview = builder.objectRef<Post>('PostPreview').implement({
  fields: (t) => ({
    post: t.field({
      type: PostRef,
      resolve: (post) => post,
    }),
    preview: t.string({
      nullable: true,
      resolve: (post) => post.content?.slice(10),
    }),
  }),
});

builder.prismaObject('User', {
  fields: (t) => ({
    id: t.exposeID('id'),
    postPreviews: t.field({
      select: (args, ctx, nestedSelection) => ({
        posts: nestedSelection(
          {
            // limit the number of postPreviews to load
            take: 2,
          },
          // Look at the selections in postPreviews.post to determine what relations/fields to select
          ['post'],
          // (optional) If the field returns a union or interface, you can pass a typeName to get selections for a specific object type
          'Post',
        ),
      }),
      type: [PostPreview],
      resolve: (user) => user.posts,
    }),
  }),
});

nestedSelection returns the relation query for the type it selected, which for a Post is { select?, include?, where?, orderBy?, take?, skip?, cursor? }. Any keys you pass in are kept as they were given, so a select passed to nestedSelection will still narrow the parent's shape. With no argument, or with true, it returns the planned selection on its own.

Pinning a type in the path

The path is followed through fragments, so a segment is found whether the field is selected directly, or under a fragment on an implementation of the field's type. When several implementations share the same field name, a segment can be written as { name, type } to name the implementation the field must be found under. Only selections of that field under a fragment on that type, or one of its subtypes, will be planned:

builder.prismaObject('User', {
  fields: (t) => ({
    entries: t.field({
      type: [Entry],
      select: (args, ctx, nestedSelection) => ({
        // Plan what `post` selects under `... on PostEntry`, not under other implementations
        posts: nestedSelection({ take: 2 }, [{ name: 'post', type: 'PostEntry' }]),
      }),
      resolve: (user) => user.posts.map((post) => ({ kind: 'post', post })),
    }),
  }),
});

The same segments can be used in queryFromInfo's path and paths options. The type is exported as PathSegment.

Selecting as a specific type

When the field returns an interface or union, the third argument names the object type the selection should be read as. Its type-level selection and the fields selected under a fragment on it are planned, and fragments on other types are left out. With an empty path, this applies to the field's own return type:

// Activity is a union of Post and Comment
builder.prismaObject('User', {
  fields: (t) => ({
    recentActivity: t.field({
      type: [Activity],
      select: (args, ctx, nestedSelection) => ({
        // What the query selects under `... on Post`, as a query for the posts relation
        posts: nestedSelection({ take: 5 }, [], 'Post'),
        // and under `... on Comment`, for the comments relation
        comments: nestedSelection({ take: 5 }, [], 'Comment'),
      }),
      resolve: (user) => [...user.posts, ...user.comments],
    }),
  }),
});

Indirect relations (eg. Join tables)

If you want to define a GraphQL field that directly exposes data from a nested relationship (many to many relations using a custom join table is a common example of this) you can use the nestedSelection function passed to select.

Given a prisma schema like the following:

model Post {
  id        Int         @id @default(autoincrement())
  title     String
  content   String
  media     PostMedia[]
}

model Media {
  id           Int         @id @default(autoincrement())
  url          String
  posts        PostMedia[]
  uploadedBy   User        @relation(fields: [uploadedById], references: [id])
  uploadedById Int
}

model PostMedia {
  id      Int   @id @default(autoincrement())
  post    Post  @relation(fields: [postId], references: [id])
  media   Media @relation(fields: [mediaId], references: [id])
  postId  Int
  mediaId Int
}

You can define a media field that can pre-load the correct relations based on the graphql query:

const PostDraft = builder.prismaObject('Post', {
  fields: (t) => ({
    title: t.exposeString('title'),
    media: t.field({
      select: (args, ctx, nestedSelection) => ({
        media: {
          select: {
            // This will look at what fields are queried on Media
            // and automatically select uploadedBy if that relation is requested
            media: nestedSelection(
              // This argument is the default query for the media relation
              // It could be something like: `{ select: { id: true } }` instead
              true,
            ),
          },
        },
      }),
      type: [Media],
      resolve: (post) => post.media.map(({ media }) => media),
    }),
  }),
});

const Media = builder.prismaObject('Media', {
  select: {
    id: true,
  },
  fields: (t) => ({
    url: t.exposeString('url'),
    uploadedBy: t.relation('uploadedBy'),
  }),
});

On this page