Attaching data from model to mutation with GraphQL - reactjs

I'm currently working on a social media site using react, graphQL and Apollo.
I have an issue retaining a field from a User when making a post. With this being graphQL, there comes a host of areas that issues can arise from (typedefs, resolvers, mutations, queries, how they're called in the component, etc) and when trying to make this post I have had it become about 3 miles long as I cut and paste all of the various code from all of those files to try and get as much detail as I can. However, that ends up being an incredible amount of code to dig through to try to find the exact error, so I'm going to skip all of that and ask a simple question in the hopes that an answer can help me crack this without someone having to read a novels worth of text.
When a User makes a post, the username is saved in the post through the resolver like such:
addPost resolver
addPost: async (parent, args, context) => {
if (context.user) {
const post = await Post.create({ ...args, username: context.user.username });
await User.findByIdAndUpdate(
{ _id: context.user._id },
{ $push: { posts: post._id } },
{ new: true }
);
return post;
}
throw new AuthenticationError('You need to be logged in!');
},
I recently finished up adding profile picture functionality to the site. A users profile image is saved to their model. I have updated the Post model to include a userImage field alongside the username field.
I attempted to retain the users profile image to attach to the post like so:
addPost resolver including image
addPost: async (parent, args, context) => {
if (context.user) {
const post = await Post.create({ ...args, username: context.user.username, userImage: context.user.image });
await User.findByIdAndUpdate(
{ _id: context.user._id },
{ $push: { posts: post._id } },
{ new: true }
);
return post;
}
throw new AuthenticationError('You need to be logged in!');
},
However, when making a post, the userImage field comes back as null. Is there somewhere else to call that value in the resolver to retain that? If I drop the "username: context.user.username" from the resolver, the post returns an error saying the username is required. In testing, I made the userImage field a required field so that a post would not post without a userImage, and I receive the same error saying the userImage field is required, even with that updated resolver including the "userImage: context.user.image."
The image field on the Post model is "userImage" and on the user model it is just "image", so I believe the way it is set up in the resolver should work, but for some reason it will not take.
Main question: How can I retain this field?
Questions based on this not working: Am I only able to call one field in the resolver (that field being username)?
When I list the userImage field as not required, the post will post, and the userImage field in the response from graphQL just says "null".
I hope this is enough for someone to point me in the right direction. I will keep an eye on this post and would be happy to add any relevant code that anyone may need to assist. I have updated everything on the typedefs, mutations.js, and queries.js files to include images and userImages respectively.
Thank you to anyone who takes a look, I'm really trying to understand what may need to be done here!

Related

Can URL API endpoint self-correct?

I am using fetch API inside a React application to retrieve and display some quiz questions.
This is my url endpoint: https://opentdb.com/api.php?amount=${amount}&difficulty=${difficulty}&type=multiple
I have noticed that:
-when I misspell part of the URL before "?" then the response doesn't get back.
example:https://opentdb.com/api.ph?amount=${amount}&difficulty=${difficulty}& (missing "p" of php)
-when I misspell part of the url after "?" then, sometimes I get an empty array back, sometimes I get the data back. How can I get data back with a wrong URL?
example: https://opentdb.com/api.php?amoun=${amount}&difficulty=${difficulty}&type=multiple (missing "t" in amount)
I haven't deployed the application yet, I am using vsc and run npm start to develop the application.
Is it possible that the URL auto-corrects? or maybe it gets cached?
my code:
export const fetchQuizQuestions = async (
amount: number,
difficulty: Difficulty
) => {
const endPoint = `https://opentdb.com/api.php?amount=${amount}&difficulty=${difficulty}&type=multiple`;
try {
const response = await fetch(endPoint);
console.log(response);
const data = await response.json();
console.log(data);
if (data.results.length === 0) {
throw new Error("The part after ? contains some mistake");
}
//below I create the new property "all_answers" and make sure the answers order is never the same
return data.results.map((question: Question) => ({
...question,
all_answers: shuffleArray([
...question.incorrect_answers,
question.correct_answer,
]),
}));
} catch (error: any) {
console.log(error.name);
console.log(error.message);
}
};
Before the ? It's the url. So if you make a mistake there, basically it's like sending a letter to a different adress, so you will not get any answers.
After the ? it's the query string. So you're asking for a result, with some parameters (your query)
So if you're saying like "ok, send me back answers with amount = XXX" but you misspell amount, it's just like "ok send me back answers" because you're not asking for amount anymore (but amoun which is nothing for the endpoint)

Sanity Query Does Not Return Updated Data Consistently

I have a "film" category in my Sanity database which has an array of comments.
In my UI, I present a pic of the film with the comments listed below the film. I created a function to add comments to the film and then fetch the film with the comments.
As you can see, in my addCommentToFilm function, I commit a patch query to Sanity to add a comment to the film; the commit returns a promise. I console log the response from Sanity, and it has the updated comments array (including the comment I just added). I then call a function to fetch the film from Sanity, but the film's comments array does not always have the last comment added: Sometimes it does, and sometimes I have to call the function a few times until I receive the updated array with the comment most recently added.
Does anyone know why this happens and whether there is a way to receive the updated list of comments consistently?
`
const addCommentToFilm = (filmId) => {
client
.patch(filmId)
.setIfMissing({ comments: [] })
.insert('after', 'comments[-1]', [{
comment,
_key: uuidV4(),
postedBy: {
_type: 'postedBy',
_ref: user._id,
}
}])
.commit()
.then((res) => {
console.log(res); // I get the film with the comments, including the comment I just added
fetchFilms(filmId);
})
.catch((error) => {
console.log('Comment post error: ', error);
})
}
const fetchFilm = (filmId) => {
client
.fetch(`*[_type == "film" && _id == "${filmId}"]`)
.then((res) => console.log(res)) // I get the film with the comments, sometimes with and sometimes without the comment I just added
}
`
OK, received this information on the Sanity help thread on Slack:
This might be related to me setting 'useCdn: true' in my client.js file, as there is a slight delay between purging/revalidating.
Link to docs: https://www.sanity.io/docs/api-cdn#da702944cd86
Once I changed my setting to 'useCdn: false' the problem was solved, and now I am getting the updated data from the database in response to my query.
Great support from Sanity on Slack!

Upload react-pdf dynamically generated file to Sanity using NextJS

I'm working on an e-commerce app built on NextJS and Sanity, so far I've made some mock products with all the necessary requirements, a user login system and checkout. I've been trying to make an invoice system so that when the user confirms an order 3 things must happen:
send all the order data to a react-pdf component and generate the invoice(working)
post the invoice file to the sanity schema so that the user has access to it when he goes to his order history page(not working)
email both the company and the client about the order(not implemented yet but I can do it)
ReactPDF allows me to access the pdf through a hook that returns me the blob of the file and the URL. I've tried to POST both of them but the url returned 404 and the blob didn't upload at all.
Searched the docs of both ReactPDF and Sanity and I couldn't find anything, although I think it has to do something with this endpoint from Sanity:
myProjectId.api.sanity.io/v2021-06-07/assets/files/myDataset
This is how I POST the order to my sanity studio
const { data } = await axios.post(
'/api/orders',
{
user: userInfo,
invoice_id: orders.length + 1,
orderItems: cartItems.map((item) => ({
...item,
slug: undefined
})),
billingData,
paymentMethod,
itemsPrice,
taxPrice,
totalPrice
},
{
headers: {
authorization: `Bearer ${userInfo.token}`
}
}
);
I've tried making 2 POST requests, one for the invoice_file alone, trying to post the blob or the url but none did work. The schema for invoice file was updated for the type of post each time so I'm 99% sure that wasn't the issue, anyway here's how the schema for invoice_file looks as for file:
{
name: 'invoice_file',
title: 'Invoice',
type: 'file',
options: {
storeOriginalFilename: true
}
},
If there would be any other code snippets relevant please let me know.
I really don't know how to find the solution for this as it's the first time trying to do such thing, so help would be much appreciated.
Thanks in advance!
I apologies as I'm not really active here but it's hard to pass on your question especially as I'm working on something similar. There's probably other ways to do this but I suggest you work use the official Sanity client. There's a specific section in the README that tells us how to do the file uploads or here.
So here's kinda the very small snippet:
import {
Document,
pdf,
} from "#react-pdf/renderer";
const doc = <Document />;
const asPdf = pdf([]); // {} is important, throws without an argument
asPdf.updateContainer(doc);
const blob = await asPdf.toBlob();
// `blob` here is coming from your react-pdf blob
const fileName = "customfilename.pdf";
client.assets.upload("file", blob, { filename: fileName }).then((fileAsset) => {
console.log(fileAsset", fileAsset);
// you can then use the fileAsset to set and reference the file that we just uploaded to our document
client.patch("document-id-here").set({
invoice_file: {
_type: "file",
asset: {
_type: "reference",
_ref: fileAsset._id,
},
},
}).commit();
});

Updating model with GraphQL Mutation

I have been having trouble figuring out how to update a User with graphQL. The functionality I'm currently aiming for is for the user to be able to update their account/profile information. I have some things set up for the user like a bio field for their profile, and a profile picture field that's set up to take a URL and display that as their profile picture.
I have no problems when it comes to creating using graphQL. A user can sign up, log in, make posts, etc without issue. I can also update the User in regards to other models, for example, a new post pushes to the users Post data just fine.
I have not been able to figure out how to update a user directly though. Essentially I can get around this by creating a new model for "profile pic" and pushing that to the User, but that seems like it's just extra steps that might slow things down, as well as shortchanging myself being able to learn something new.
This is the User model. I have omitted a few fields due to the exact block of code being large, but this includes the "image" and "bio" fields (the fields I would like to update) as well as the reference to the Post model which I mentioned above that functions appropriately.
User.js
const userSchema = new Schema(
{
username: {
type: String,
required: true,
unique: true,
trim: true
},
email: {
type: String,
required: true,
unique: true,
match: [/.+#.+\..+/, 'Must match an email address!']
},
password: {
type: String,
required: true,
minlength: 8
},
image: {
type: String
},
bio: {
type: String,
maxLength: 500
},
posts: [
{
type: Schema.Types.ObjectId,
ref: 'Post'
}
],
},
Below is the mutation in Explorer, including the variables and the result.
Profile Pic Resolver
addProfilePic: async (parent, { image }, context) => {
if (context.user) {
const updatedUser = await User.findOneAndUpdate(
{ _id: context.user._id },
{ image: image },
{ new: true, runValidators: true }
);
return updatedUser;
}
throw new AuthenticationError('You need to be logged in!');
},
typeDefs.js (relevant only)
type Mutation {
addProfilePic(_id: ID!, image: String!): Auth
}
I notice that in the Explorer page it returns "null" for user with a 200 status. I am led to believe that means that it's not able to even access the "image" field on the user to be able to update it. When compared to my other mutations in regards to users, this is set up very similarly and I'm not sure what the difference is.
I feel like I am missing something very basic here in regards to being able to update. I haven't been able to find an update mutation example that works. Could anyone assist? My main questions would be:
Why does the mutation return "null" for user?
How can I set up my resolver to appropriately update information on an already-created object?
Thank you to anyone who is able to take a look and assist, I will be closely watching this post for replies and will update any other code someone may need to be able to assist. I've been stuck in regards to updating information for a long time, but my site is getting to the point where it's nearly ready and I need to tackle this updating issue in order to progress. Thank you!
Quick Edit: I want to add that "Auth" is referenced. The appropriate authorization headers are in place to retrieve the data. Just wanted to add that in as I highly doubt authorization has anything to do with this!
I have solved this issue and would like to leave the answer here for anyone who may find it useful.
In the mutation typeDefs, I changed the "Auth" to "User",
type Mutation {
addProfilePic(_id: ID!, image: String!): User
}
and then in the mutation itself, took away the user field like such:
mutation addProfilePic($_id: ID!, $image: String!) {
addProfilePic(_id: $_id, image: $image) {
_id
username
image
}
}
This has allowed the user to update their profile photo information. Hope this helps!

Apollo Client cache

I just started using apollo client on a React application and I'm stuck on caching.
I have a home page with a list of products where I do a query to get the id and name of those products and a product page where I do query for the ID, name, description and image.
I would like that if a user visits the home page fist then a specific product page to only do a query for that product's description and image, also display the name during the loading (since I should have cached it already).
I followed "Controlling the Store" part of the documentation (http://dev.apollodata.com/react/cache-updates.html) but still couldn't resolve it.
The query that is done when we go to the product page still asks for both the product's id and name whereas they should be cached since I already asked for them.
I think I'm missing something but I can't figure it out.
Here is a bit of the code:
// Create the apollo graphql client.
const apolloClient = new ApolloClient({
networkInterface: createNetworkInterface({
uri: `${process.env.GRAPHQL_ENDPOINT}`
}),
queryTransformer: addTypename,
dataIdFromObject: (result) => {
if (result.id && result.__typename) {
console.log(result.id, result.__typename); //can see this on console, seems okey
return result.__typename + result.id;
}
// Make sure to return null if this object doesn't have an ID
return null;
},
});
// home page query
// return an array of objects (Product)
export default graphql(gql`
query ProductsQuery {
products {
id, name
}
}
`)(Home);
//product page query
//return an object (Product)
export default graphql(gql`
query ProductQuery($productId: ID!) {
product(id: $productId) {
id, name, description, image
}
}
`,{
options: props => ({ variables: { productId: props.params.id } }),
props: ({ data: { loading, product } }) => ({
loading,
product,})
})(Product);
And my console output:
The answer to your question actually has two parts:
The client cannot actually tell for sure that these queries resolve to the same object in the cache, because they have a different path. One starts with products, the other with product. There's an open PR for client-side resolvers, which will let you give the client hints about where to find things in the cache, even if you haven't explicitly queried for them. I expect that we will publish that feature within a week or two.
Even with client-side resolvers, Apollo Client won't do exactly what you described above, because Apollo Client no longer does query diffing since version 0.5. Instead, all queries are fully static now. That means even if your query is in the cache partially, the full query will be sent to the server. This has a number of advantages that are laid out in this blog post.
You will still be able to display the part that's in the cache first, by setting returnPartialData: true in the options.
This question is quite old, however, there is a solution to map the query to the correct location using cacheRedirects
In my project, I have a projects query and a project query.
I can make a cacheRedirect like below:
const client = new ApolloClient({
uri: "http://localhost:3000/graphql",
request: async (operation) => {
const token = await localStorage.getItem('authToken');
operation.setContext({
headers: {
authorization: token
}
});
},
cacheRedirects: {
Query: {
project: (_, { id }, { getCacheKey }) => getCacheKey({ id, __typename: 'Project' })
}
}
});
Then when I load my dashboard, there is 1 query which gets projects. And then when navigating to a single project. No network request is made because it's reading from the cache 🎉
Read the full documentation on Cache Redirects

Resources