Indirect relations
Indirect relations and join tables
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.
import type { Post } from '../lib/prisma/client';
const PostRef = builder.prismaObject('Post', {
fields: (t) => ({
title: t.exposeString('title'),
content: t.exposeString('content', { nullable: true }),
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(0, 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) => {
return 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 PostWithMedia = 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) => {
return post.media.map(({ media }) => media);
},
}),
}),
});
const Media = builder.prismaObject('Media', {
select: {
id: true,
},
fields: (t) => ({
url: t.exposeString('url'),
uploadedBy: t.relation('uploadedBy'),
}),
});Shared media on published posts
The publishing schema stores attachment rows in PostMedia, but clients request Media objects.
nestedSelection follows the media field's selection through that join, including the uploader
when requested:
const Post = builder.prismaNode('Post', {
nullable: true,
id: { field: 'id' },
select: { id: true },
// Node refetches must apply the same visibility rule as root fields.
findUnique: (id, context) => ({
id: Number(id),
OR: [{ published: true }, { authorId: context.userId }],
}),
fields: (t) => ({
title: t.exposeString('title'),
published: t.exposeBoolean('published'),
author: t.relation('author'),
comments: t.relation('comments', { query: { orderBy: { id: 'asc' } } }),
media: t.field({
type: [Media],
select: (_args, _ctx, nestedSelection) => ({
media: { orderBy: { id: 'asc' }, select: { media: nestedSelection(true) } },
}),
resolve: (post) => {
return post.media.map(({ media }) => media);
},
}),
}),
});Both “Starting a seed library” and “A guide to composting” attach the same image, uploaded by Leo.
Querying posts { title media { url uploadedBy { name } } } through Maya's author page returns
that uploader for both posts. The GraphQL shape need not expose the join table just because the
database uses one.