-
Assume you have a domain that is modeled like so:
And then have both Movie and Show implement that interface:
(Similar implementation on Show). In this point, we already get a TS error saying the object shape must extend the interface - even though we have everything defined properly. If we ignore the error and run the code as-is, it still works, so let's try to move on for now. We now have a query field on the type
But this raises a new error - the returned type, which is plain Prisma result objects, does not match the Makes sense - we couldn't really expect this to work as-is, but the docs don't mention how we can tackle this use-case.
So I'm wondering, what are the options here? |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments
-
The general rule for interfaces is that object types need to by type-compatible with the type defined for an interface. Using a class for your interface is probably a bad starting place. What you can do is use a simpler interface type that only contains the common properties of the 2 tables interface Media {
title: string
}
const Media = builder.interfaceRef<Media>('Media')
Media.implement({
fields: (t) => ({
title: t.exposeString('title'),
poster: t.field({
type: Poster,
nullable: true,
// since we aren't defining a resolver here, movie and show need to implement the poster field as well
}),
})
})
const Movie = builder.prismaObject("Movie", {
interfaces: ["Media"], // Raises a TS error
fields: (t) => ({
poster: t.field({
type: Poster,
nullable: true,
select: { poster: true, posterThumb: true },
resolve: (movie) =>
movie.poster && movie.posterThumb
? new MediaPoster(posters.url(movie.poster), movie.posterThumb)
: null,
}),
})
}
)
builder.prismaObject("User", {
fields: (t) => ({
followedMedia: t.field({
type: [Media],
nullable: false,
select: {
followedMovies: {
select: {
movie: true,
},
},
followedShows: {
select: {
show: true,
},
},
},
resolve: async (user) => {
return [
// Adding brands will allow pothos to automatically determine the type during resolution
...Movie.addBrand(user.followedMovies),
...Show.addBrand(user.followedShows),
];
},
}) I've removed the type, because you can just query for |
Beta Was this translation helpful? Give feedback.
-
@hayes Thank you for the clarification. This does seem to help, but Pothos still complains about Movie not implementing Media. The problem seems to happen when an implementation of Media defines a field through Simplified example using a primitive field: export interface Media {
id: number | string;
title: string;
isFollowed: boolean; // Resolved by children
}
...
Media.implement({
fields: (t) => ({
id: t.exposeID("id", { nullable: false }),
title: t.exposeString("title", { nullable: false }),
isFollowed: t.exposeBoolean("isFollowed", { nullable: false }), // Also tried `t.boolean` here
}),
})
const movieType = builder.prismaObject("Movie", {
interfaces: [Media], // Error!
fields: (t) => ({
// Other fields...
isFollowed: t.boolean({
nullable: false,
select: (_, ctx) => ({
followers: {
where: {
userId: ctx.userId,
},
select: {
id: true,
},
},
}),
resolve: (movie) => movie.followers.length > 0,
}),
}),
}); In this example, the |
Beta Was this translation helpful? Give feedback.
-
The typescript type/interface describes the data returned by your resolvers and what is available on the parent object. If you add If the parent object (eg. the movie row from the db) doesn't have the You can use normal field methods without a resolver to define the field on the interface. This implements it with a default resolver that just throws an error. When you add an Just removing the isFollowed property from the typescript type, and using t.boolean instead of t.exposeBoolean should be all you need: export interface Media {
id: number | string;
title: string;
}
...
Media.implement({
fields: (t) => ({
id: t.exposeID("id", { nullable: false }),
title: t.exposeString("title", { nullable: false }),
isFollowed: t.boolean({ nullable: false }),
}),
})
const movieType = builder.prismaObject("Movie", {
interfaces: [Media], // Error!
fields: (t) => ({
// Other fields...
isFollowed: t.boolean({
nullable: false,
select: (_, ctx) => ({
followers: {
where: {
userId: ctx.userId,
},
select: {
id: true,
},
},
}),
resolve: (movie) => movie.followers.length > 0,
}),
}),
}); |
Beta Was this translation helpful? Give feedback.
The typescript type/interface describes the data returned by your resolvers and what is available on the parent object.
If you add
isFollowed
to the interface, all the types that implement Media need to have that property (which they don't)If the parent object (eg. the movie row from the db) doesn't have the
isFollowed
property, you can't "expose" it because it doesn't exist.You can use normal field methods without a resolver to define the field on the interface. This implements it with a default resolver that just throws an error.
When you add an
isFollowed
field on types that implement Media you are overwriting that default resolver, so it won't throw for those types.Just removing th…