Prisma and ApolloClient: Prevent overwriting the include conditions at the backend by the frontend for relations - apollo-client

I have a problem, thx for any help.
With prisma we can use include with where conditions for models with a relation. If I make include conditions I get the right result. If I return it to the frontend it gets overwritten. I want to return exact my result from the backend.
I have at the frontend a query (ApolloClient, gql) like. It will return an array of comments for each post, I just want to have the first Comment for each post.
const POSTS = gql`
query posts {
posts(postId: $postId) {
id
comments{ // at the backend I have conditions for the comments
id
}
}
}
`;
Backend: Primsa and graphql nexus
Prisma Schema
model Post {
id String #id #default(cuid())
comments Comment[]
}
model Comment {
id String #id #default(cuid())
post Post #relation(fields: [postId], references: [id])
postId String
}
Nexus Model
const Post = objectType({
name: 'Post',
definition(t) {
t.model.id()
t.model.comments()
})
const Comment = objectType({
name: 'Comment',
definition(t) {
t.model.id()
t.model.post()
t.model.postId()
})
Resolver
export const posts = queryField('posts', {
type: 'Post',
list: true,
args: {
...
},
resolve: async (_parent, args: any, { prisma, request }, info) => {
const posts = await prisma.post.findMany({
include: {
comments: {
take: 1
}
}
})
console.log(posts)
//Perfect result I want to return the include condition. But at the frontend I have all
//comments
return posts
},
})
The console.log(posts) is exact what I want to return!. Every post has an Array of ONE Comment.
I return the posts and at the frontend every post has an Array of ALL Comments, what I don't want. How can I prevent that the frontend query overwrite the backend return? The fields are the same.

I can't add a comment, so I am adding this to another answer.
Like I said with my PrismaSelect plugin, you can't use nexus-plugin-prisma t.model, t.crud. You will need to use Pal.Js CLI to autoGenerate all CRUD and ObjectTypes for all models.
const Post = objectType({
name: 'Post',
definition(t) {
t.model.id()
t.model.comments() // this field will overwritten by next one so this not needed
t.list.field('comments', {
type: 'Comment',
list: true,
resolve: (parent, args, { prisma }) => {
// here parent type include all other fields but not this field
return prisma.comment.findMany({ // this query is very wrong will case N+1 issue
where: {
postId: parent.id,
},
take: 1,
})
},
})
})
Example
model User {
id Int #default(autoincrement()) #id
createdAt DateTime #default(now())
email String #unique
name String?
password String
posts Post[]
comments Comment[]
}
model Post {
id Int #default(autoincrement()) #id
published Boolean #default(false)
title String
author User? #relation(fields: [authorId], references: [id])
authorId Int?
comments Comment[]
}
model Comment {
id Int #default(autoincrement()) #id
contain String
post Post #relation(fields: [postId], references: [id])
postId Int
author User? #relation(fields: [authorId], references: [id])
authorId Int?
}
Here is my Pal.Js CLI generated type for Post model
import { objectType } from '#nexus/schema'
export const Post = objectType({
name: 'Post',
definition(t) {
t.int('id', { nullable: false })
t.boolean('published', { nullable: false })
t.string('title', { nullable: false })
t.field('author', {
nullable: true,
type: 'User',
resolve(parent: any) {
return parent['author']
},
})
t.int('authorId', { nullable: true })
t.field('comments', {
nullable: false,
list: [true],
type: 'Comment',
args: {
where: 'CommentWhereInput',
orderBy: 'CommentOrderByInput',
cursor: 'CommentWhereUniqueInput',
take: 'Int',
skip: 'Int',
distinct: 'CommentDistinctFieldEnum',
},
resolve(parent: any) {
return parent['comments']
},
})
},
})
when you use my Pal.js CLI, your frontend request will be like this
query {
findOnePost(where: {id: 1}) {
comments(where: {}, take: 1){
id
}
}
}
``

The best way to handle this issue and just query what your frontend request to use my PrismaSelect plugin.
Prisma Select takes the info: GraphQLResolveInfo object in general graphql arguments (parent, args, context, info) to select object accepted by prisma client. The approach allows a better performance since you will only be using one resolver to retrieve all your request. By doing so, it also eliminates the N + 1 issue.
Also, you can use my CLI to autogenerate all CRUD from your schema.prisma file https://paljs.com/generator/nexus

I mean I can add to my Post-ObjectType a field condition like:
const Post = objectType({
name: 'Post',
definition(t) {
t.model.id()
t.model.comments()
t.list.field('comments', {
type: 'Comment',
list: true,
resolve: (parent, args, { prisma }) => {
return prisma.comment.findMany({
where: {
postId: parent.id,
},
take: 1,
})
},
})
})
This is working. But if I understood it correct I have for every post one extra request. But I have already at the mutation resolver the right result. And I don't have the comments field at the parent (t.list.field- resolver)

Related

How to read GrqphQL resolver

While I try to learn GraphQL-Tools.I found this article.In this article,example resolvers are described as follows.
const resolvers = {
Query: {
posts: () => posts,
author: (_, { id }) => find(authors, { id })
},
Mutation: {
upvotePost: (_, { postId }) => {
const post = find(posts, { id: postId })
if (!post) {
throw new Error(`Couldn't find post with id ${postId}`)
}
post.votes += 1
return post
}
},
Author: {
posts: author => filter(posts, { authorId: author.id })
},
Post: {
author: post => find(authors, { id: post.authorId })
}
}
I understood that Queryand MutationandSubscriptionis main function of Graphql resolvers.
My question is what is Author,Post in this resolver ?
I can understand QueryandMutationsection of this resolver. but how to think about Authorand Post?
It seems that it defined functions.
Thanks
That is Trival Resolver
https://graphql.org/learn/execution/#trivial-resolvers
Until Graphql has a completely understandable return value.
Graphql goes on to the next search with the returned type.
Those functions [Author, Post] tell to Graphql how to find the object's value.
For exameple if "PostFindById" reoslver promised to graphql that will return "author" field But instead it return "Post" Graphql will excute Trival Rsolver to find value of author field with given "Post".
So it will execute this resolver with given 'post'
Post: {
author: post => find(authors, { id: post.authorId })
}

GraphQL - defining types

I'm trying to follow Ben Awad's lireddit tutorial.
At the time he made the tutorial, there may have been different inferences about Field types.
I'm trying to add a Field to my relationship attribute (adding creator Field to a Post), so that I can then access creator attributes on that post record.
Ben does this as follows:
#Field()
#ManyToOne(() => User, (user) => user.posts)
creator: User;
That worked for him. When I try this, I get an error that says:
throw new errors_1.NoExplicitTypeError(prototype.constructor.name, propertyKey,
parameterIndex, argName);
NoExplicitTypeError: Unable to infer GraphQL type from TypeScript
reflection system. You need to provide explicit type for 'creator' of
'Post' class.
When I look at the GraphQL docs for how to provide an explicit type for creator, I can't find a similar example (simple enough for me to decipher a principle that I can apply).
I'm confused by the docs, because the have the following example:
Can anyone see what I need to do to ask for the field to be recognised as an object that I can read from?
#ObjectType()
class Rate {
#Field(type => Int)
value: number;
#Field()
date: Date;
user: User;
}
I think they use user: User the same way I use creator: User. Is there a reason that Field() can't have the same thing as ObjectType()?
I tried:
#Field(() => [User])
#ManyToOne(() => User, (user) => user.posts)
creator: User;
This doesn't give any errors (neither does the code the way Ben has it), until I get to the playground, in which case, I can't return the user data - so clearly it's wrong. It also isn't clear whether the array means the array of attributes on the user object, or an array of users (which would also be wrong). I can see from the GraphQL docs that it should be possible to define a field attribute as an object type, but I can't find an example showing how to do that.
I have seen this post, which looks like a similar problem, but I can't see from the suggested answers, how to apply those ideas to this problem.
I have seen this post, which has a similar problem, and is answered with a reference to an example that shows how to write resolvers that find relations, but my resolver already worked to find the creatorId, so I think maybe I'm not looking in the right place for an answer.
In my post resolver, I have:
import {
Resolver,
Query,
Arg,
Mutation,
InputType,
Field,
Ctx,
UseMiddleware,
Int,
FieldResolver,
Root,
ObjectType,
} from "type-graphql";
import { Post } from "../entities/Post";
import { MyContext } from "../types";
import { isAuth } from "../middleware/isAuth";
import { getConnection } from "typeorm";
#InputType()
class PostInput {
#Field()
title: string;
#Field()
text: string;
}
#ObjectType()
class PaginatedPosts {
#Field(() => [Post])
posts: Post[];
#Field()
hasMore: boolean;
}
#Resolver(Post)
export class PostResolver {
#FieldResolver(() => String)
textSnippet(#Root() post: Post) {
return post.text.slice(0, 50);
}
#Query(() => PaginatedPosts)
async posts(
#Arg("limit", () => Int) limit: number,
#Arg("cursor", () => String, { nullable: true }) cursor: string | null
): Promise<PaginatedPosts> {
// 20 -> 21
const realLimit = Math.min(50, limit);
const reaLimitPlusOne = realLimit + 1;
const qb = getConnection()
.getRepository(Post)
.createQueryBuilder("p")
.orderBy('"createdAt"', "DESC")
.take(reaLimitPlusOne);
if (cursor) {
qb.where('"createdAt" < :cursor', {
cursor: new Date(parseInt(cursor)),
});
}
const posts = await qb.getMany();
return {
posts: posts.slice(0, realLimit),
hasMore: posts.length === reaLimitPlusOne,
};
}
#Query(() => Post, { nullable: true })
post(#Arg("id") id: number): Promise<Post | undefined> {
return Post.findOne(id);
}
#Mutation(() => Post)
#UseMiddleware(isAuth)
async createPost(
#Arg("input") input: PostInput,
#Ctx() { req }: MyContext
): Promise<Post> {
return Post.create({
...input,
creatorId: req.session.userId,
}).save();
}
#Mutation(() => Post, { nullable: true })
async updatePost(
#Arg("id") id: number,
#Arg("title", () => String, { nullable: true }) title: string
): Promise<Post | null> {
const post = await Post.findOne(id);
if (!post) {
return null;
}
if (typeof title !== "undefined") {
await Post.update({ id }, { title });
}
return post;
}
#Mutation(() => Boolean)
async deletePost(#Arg("id") id: number): Promise<boolean> {
await Post.delete(id);
return true;
}
}
First of all, creator should be of a User type, and not a list of users, i.e.
#Field(() => User)
#ManyToOne(() => User, (user) => user.posts)
creator: User;
When you are retrieving a post, you should include a relation in your query, so the User entity is also loaded:
#Query(() => Post, { nullable: true })
post(#Arg("id") id: number): Promise<Post | undefined> {
return Post.findOne(id, { relations: "creator" });
}
Also, when you're using query builder to fetch posts, you should add User entities:
const qb = getConnection()
.getRepository(Post)
.createQueryBuilder("p")
.leftJoinAndSelect("p.creator", "p_creator")
.orderBy('"createdAt"', "DESC")
.take(reaLimitPlusOne);
Bonus note:
There's a common problem of over-fetching the data in GraphQL, so queries can become slow with time.
In that manner, you could also consider moving the creator field to FieldResolver, so it's retrieved from the database only if it's requested. In case you do that, one other good practice with ManyToOne relations is to use a dataloader, so if you, for example, load 10 posts from the same creator, you'll end up with only one fetching operation of that creator instead of 10 requests to the database. There's a great tutorial and explanation provided by Ben Awad too: https://www.youtube.com/watch?v=uCbFMZYQbxE.
This isn't necessary for this tutorial in particular, but it's a must-know if you're building some serious app.

Updated react apollo cached but ui didnt update <Sharing || Solved>

Must included all data attribute inside reference as the typeDefs value. It's okies, if the value is null.
In apollo-client: 2.6.10
TypeDefs Graphql and Mutation below:
type Product {
_id: ID
name: String!
benefit: String
country: String
vitamins: [String]
createdAt: String!
}
const [addFruit] = useMutation(ADD_FRUIT_MUTATION, {
update(cache, result) {
const cachedData = cache.readQuery({
query: GET_FRUTIS_QUERY,
});
cachedData.getFruits = [
{
...result.data.create_fruit, // name, vitamins, country, benefit
createdAt: new Date().toISOString(), // added this
_id: result.data.create_fruit.name, // added this, this only temporary and must unique.
},
...cachedData.getFruits,
];
cache.writeQuery({
query: GET_FRUTIS_QUERY,
data: { ...cachedData },
});
},
onError({ networkError, graphQLErrors }) {},
variables: fruitVariable,
});
hope this helps someone. =)
I wondering why it must all included the data attribute key? if dont, it will updated cached but not into UI.

how do I shape my graphql backend response?

I am querying my graphql backend and need the response to have a flatten shape,
my query:
gql`
{
questions {
edges {
id
title
author: user {
email
}
}
}
}
`
my response:
'5d3eafb7889a135ff8cd950c': {
id: '5d3eafb7889a135ff8cd950c',
title: 's',
author: {
email: 'dggdfgdgfd#gmail.com',
__typename: 'User'
},
__typename: 'Question'
},
problem is with author, I need as a string instead of an object:
id: '5d3eafb7889a135ff8cd950c',
title: 's',
author: 'dggdfgdgfd#gmail.com' // <===
`
You have two options:
1) After getting the data from the GraphQL server, transform the data as you need to fit your needs. A Javascript implementation might be:
function flattenGqlResponse (response) {
return Object.keys(response).map((question) => {
return {
id: response[question].id,
title: response[question].title,
author: response[question].author.email
}
}
);
}
2) Flatten the structure in your GraphQL Resolver on your GraphQL Server. Likely, you do not want to pursue this route based on what I see about your schema, unless your User object only has one field, that is email

GraphQL pass args to sub resolve

I have a relationship between User and Post. This is how I query the User Posts.
const UserType = new GraphQLObjectType({
name: 'User'
fields: () => ({
name: {
type: GraphQLString
},
posts: {
type: new GraphQLList(PostType),
resolve(parent, args , { db }) {
// I want to get here the args.someBooleanArg
return someLogicToGetUserPosts();
}
}
})
});
The main query is:
const queryType = new GraphQLObjectType({
name: 'RootQuery',
fields: {
users: {
type: new GraphQLList(UserType),
args: {
id: {
type: GraphQLInt
},
someBooleanArg: {
type: GraphQLInt
}
},
resolve: (root, { id, someBooleanArg }, { db }) => {
return someLogicToGetUsers();
}
}
}
});
The problem is the args in the resolve function of the UserType posts is empty object, how do i pass the args from the main query to sub resolves functions?
When resolving the root query you can use object assign to attach the argument to the user object returned.
Then, on the user type, resolve the argument from the root value (first argument of resolve function).
Example:
const queryType = new GraphQLObjectType({
name: 'RootQuery',
fields: {
users: {
type: new GraphQLList(UserType),
args: {
id: {
type: GraphQLInt
},
someBooleanArg: {
type: GraphQLInt
}
},
resolve: (root, { id, someBooleanArg }, { db }) => {
return Promise.resolve(someLogicToGetUsers()).then(v => {
return Object.assign({}, v, {
someBooleanArg
});
});
}
}
}
});
const UserType = new GraphQLObjectType({
name: 'User'
fields: () => ({
name: {
type: GraphQLString
},
posts: {
type: new GraphQLList(PostType),
resolve(parent, args , { db }) {
console.log(parent.someBooleanArg);
return someLogicToGetUserPosts();
}
}
})
});
You can use the resolver fouth argument, info, to receive the desired variable - from Apollo docs:
Every resolver in a GraphQL.js schema accepts four positional arguments:
fieldName(obj, args, context, info)
{ result }
These arguments have
the following meanings and conventional names:
obj: The object that contains the result returned from the resolver on
the parent field, or, in the case of a top-level Query field, the
rootValue passed from the server configuration. This argument enables
the nested nature of GraphQL queries.
args: An object with the
arguments passed into the field in the query. For example, if the
field was called with author(name: "Ada"), the args object would be: {
"name": "Ada" }.
context: This is an object shared by all resolvers in
a particular query, and is used to contain per-request state,
including authentication information, dataloader instances, and
anything else that should be taken into account when resolving the
query. If you're using Apollo Server, read about how to set the
context in the setup documentation.
info: This argument should only be
used in advanced cases, but it contains information about the
execution state of the query, including the field name, path to the
field from the root, and more. It's only documented in the GraphQL.js
source code.
The info seems to be a very undocumented feature, but I'm using it now with no problems (at least until somebody decide to change it).
Here is the trick:
const UserType = new GraphQLObjectType({
name: 'User'
fields: () => ({
name: {
type: GraphQLString
},
posts: {
type: new GraphQLList(PostType),
resolve(parent, args , { db }, info) {
// I want to get here the args.someBooleanArg
console.log("BINGO!");
console.log(info.variableValues.someBooleanArg);
return someLogicToGetUserPosts();
}
}
})
});

Resources