How can I accessed upsertedId when using updateOne in MongoDB - arrays

I have new data that I want to insert in my array of blog (My collection look like this - shown below):-
{
_id: 0,
// other vars here...,
blog: [
{
_id: 0,
title: 'Article 1'
},
// add new article here
]
}
Right now I'm able to add new article in my blog array with the code shown below:-
const query = {}
const update = { $push: { blog: inputData } }
const options = { upsert: true }
All.updateOne(query, update, options)
.then(result => {
res.redirect(`/article/${result.upsertedId}`) // read MongoDB documentation that I can get the newly inserted data ID by using result.upsertedId
})
.catch(err => res.redirect(`/`)
But I can't seem to get the ID of the newly inserted data into my collection. Using result.upsertedId returns me undefined instead. According to the code above, I need the ID in order to redirect user to the new article page of that ID.
Here's the documentation telling that it will return upsertedId updateOne
this is the result I get when console.log it (There's no upsertedId anywhere in it)

Print your result to confirm an ID is being received, your result is likely something similar to this:
{ "acknowledged" : true, "matchedCount" : 1, "modifiedCount" : 1 }
You could try using collection.findOneAndUpdate and retrieve upsertedId from that result

You need to pass new: true in the option to get the id of the updated document
i.e
const query = {}
const update = { $push: { blog: inputData } }
const options = { upsert: true, new: true }
All.updateOne(query, update, options)
.then(result => {
//result is your upserted document
console.log("Upserted document : ", result);// try getting your upserted _id
//from this result
res.redirect(`/article/${result.upsertedId}`) // change //result.upsertedId into result._id or result.id whatever you get into //your result
})
.catch(err => res.redirect(`/`)

It is not applicable for nested documents.
What you are doing is an update operation.
Doc says
The _id value of the document inserted by an upsert operation. This value is only present when the upsert option is enabled and the update query does not match any documents.
And your query results in update operation. That's why you are getting nModified.
One option is that you can use findOneAndUpdate with option returnNewDocument option true.

Related

Updating multiple queries in useMutation Apollo-client without refetching them

I'm executing this mutation in my NewBook component:
const [addBook] = useMutation(ADD_BOOK, {
update: (cache, response) => {
cache.updateQuery({ query: ALL_BOOKS }, ({ allBooks }) => {
return { allBooks: allBooks.concat(response.data.addBook) };
});
},
refetchQueries: [{ query: ALL_AUTHORS }, { query: ALL_GENRES }],
options: {
awaitRefetchQueries: true,}});
Instead of having to refetch those two queries, I'd like to update them like ALL_BOOKS - but could not find any example in the docs. Does anyone know a way to accomplish that?
Thank you.
What you need to do is make multiple cache updates based on response data.
Once you add your new book to the query, the next step is to fetch all authors.
cache.updateQuery({ query: ALL_BOOKS }, ({ allBooks }) => {
return { allBooks: allBooks.concat(response.data.addBook) };
});
//Get all authors
const existingAuthors = cache.readQuery({
query: ALL_AUTHORS,
//variables: {}
});
//If we never called authors, do nothing, as the next fetch will fetch updated authors. This might be a problem in the future is some cases, depending on how you fetch data. If it is a problem, just rework this to add newAuthor to the array, like allAuthors: [newAuthor]
if(!existingAuthors.?length) {
return null
}
The next thing is that we need to compare the new book's author with existing authors to see if a new author was added.
//continued
const hasAuthor = existingAuthors.find(author => author.id === response.data.createBook.id)
//Double check response.data.createBook.id. I don't know what is returned from response
//If author already exists, do nothing
if(hasAuthor) {
return null
}
//Get a new author. Make sure that this is the same as the one you fetch with ALL_AUTHORS.
const newAuthor = {
...response.data.createBook.author //Check this
}
cache.writeQuery({
query: ALL_AUTHORS,
//variables: {}
data: {
allAuthors: [newAuthor, ...existingAuthors.allAuthors]
},
});
Then continue the same with ALL_GENRES
Note:
If you called ALL_GENERES or ALL_BOOKS with variables, you MUST put the SAME variables in the write query and read query. Otherwise Apollo wont know what to update
Double check if you are comparing numbers or strings for authors and genres
Double check all of the variables I added, they might be named different at your end.
Use console.log to check incoming variables
You can probably make this in less lines. There are multiple ways to update cache
If it doesn't work, console.log cache after the update and see what exactly did apollo do with the update (It could be missing data, or wrong variables.)
Add more checks to ensure some cases like: response.data returned null, authors already fetched but there are none, etc...

How to make an infinite scroll with react-query?

I'm trying to use react-query useInfiniteScroll with a basic API, such as the cocktaildb or pokeapi.
useInfiniteQuery takes two parameters: a unique key for the cache and a function it has to run.
It returns a data object, and also a fetchMore function. If fetchMore is called - through an intersection observer for exemple -, useInfiniteQuery call its parameter function again, but with an updated payload thanks to a native callback getFetchMore().
In the official documentation, getFetchMore automatically takes two argument: the last value returned, and all the values returned.
Based on this, their demo takes the value of the previous page number sent by getFetchMore, and performs a new call with an updated page number.
But how can I perform the same kind of thing with a basic api that only return a json?
Here is the official demo code:
function Projects() {
const fetchProjects = (key, cursor = 0) =>
fetch('/api/projects?cursor=' + cursor)
const {
status,
data,
isFetching,
isFetchingMore,
fetchMore,
canFetchMore,
} = useInfiniteQuery('projects', fetchProjects, {
getFetchMore: (lastGroup, allGroups) => lastGroup.nextCursor,
})
infinite scrolling relies on pagination, so to utilize this component, you'd need to somehow track what page you are on, and if there are more pages. If you're working with a list of elements, you could check to see if less elements where returned in your last query. For example, if you get 5 new items on each new fetch, and on the last fetch you got only 4, you've probably reached the edge of the list.
so in that case you'd check if lastGroup.length < 5, and if that returns true, return false (stop fetching more pages).
In case there are more pages to fetch, you'd need to return the number of the current page from getFetchMore, so that the query uses it as a parameter. One way of measuring what page you might be on would be to count how many array exist inside the data object, since infiniteQuery places each new page into a separate array inside data. so if the length of data array is 1, it means you have fetched only page 1, in which case you'd want to return the number 2.
final result:
getFetchMore: (lastGroup, allGroups) => {
const morePagesExist = lastGroup?.length === 5
if (!morePagesExist) return false;
return allGroups.length+1
}
now you just need to use getMore to fetch more pages.
The steps are:
Waiting for useInfiniteQuery to request the first group of data by default.
Returning the information for the next query in getNextPageParam.
Calling fetchNextPage function.
Reference https://react-query.tanstack.com/guides/infinite-queries
Example 1 with rest api
const fetchProjects = ({ pageParam = 0 }) =>
fetch('/api/projects?cursor=' + pageParam)
const {
data,
isLoading,
fetchNextPage,
hasNextPage,
} = useInfiniteQuery('projects', fetchProjects, {
getNextPageParam: (lastPage) => {
// lastPage signature depends on your api respond, below is a pseudocode
if (lastPage.hasNextPage) {
return lastPage.nextCursor;
}
return undefined;
},
})
Example 2 with graphql query (pseudocode)
const {
data,
fetchNextPage,
isLoading,
} = useInfiniteQuery(
['GetProjectsKeyQuery'],
async ({ pageParam }) => {
return graphqlClient.request(GetProjectsQuery, {
isPublic: true, // some condition/variables if you have
first: NBR_OF_ELEMENTS_TO_FETCH, // 10 to start with
cursor: pageParam,
});
},
{
getNextPageParam: (lastPage) => {
// pseudocode, lastPage depends on your api respond
if (lastPage.projects.pageInfo.hasNextPage) {
return lastPage.projects.pageInfo.endCursor;
}
return undefined;
},
},
);
react-query will create data which contains an array called pages. Every time you call api with the new cursor/page/offset it will add new page to pages. You can flatMap data, e.g:
const projects = data.pages.flatMap((p) => p.projects.nodes)
Call fetchNextPage somewhere in your code when you want to call api again for next batch, e.g:
const handleEndReached = () => {
fetchNextPage();
};
Graphql example query:
add to your query after: cursor:
query GetProjectsQuery($isPublic: Boolean, $first: Int, $cursor: Cursor) {
projects(
condition: {isPublic: $isPublic}
first: $first
after: $cursor
) ...

Array is empty after for loop

I'm setting an array before a for loop, inside the for loop I use .push() to add data to the array but after this loop the array is empty.
MessageNotification.find({for: req.user.id}, (err, notifications) => {
var userdata = [];
notifications.forEach((notif) => {
User.findById(notif.from, (err, user) => {
userdata.push({
id: user._id,
username: user.username,
thumbnail: user.thumbnail
});
});
});
console.log(userdata);
});
As you can see on the code I am running a mongoose query to find all notifications for a specific id, then, I am setting an array to get details about the sender of each notification. Inside a forEach loop I save the results in the array. Console.log on line 12 returns an empty array [] even though User.findById on line 4 gets the User data
The problem is you are doing and asynchronous call in forEach. You should either use async/await with for..of or promises in such cases.
In your case, actually there is no need to do multiple calls on User model, you can get the desired result in a single query. Try the below code:
MessageNotification.find({
for: req.user.id
}, (err, notifications) => {
const fromArr = notifications.map(({
from
}) => from); // taking out **from** values from all notifications
User.find({
_id: {
$in: fromArr
}
}, (err, users) => { // single query to get the data
const userData = users.map(({
_id: id,
username,
thumbnail
}) => {
return {
id,
username,
thumbnail
};
});
console.log(userData);
});
});
The problem here is actually that you're calling .forEach with an async calls inside. Rather than iterating over each item in the array, and running a separate query for each, you should use the $in operator which will check if any values match items within the array, with a single query.

ReactJS: Update a specific Object from an array of Objects in the state

So, I have a a state of users which gets filled up with an array of Objects from the backend. Say, I want to update the details of a specific User Object from that array and then update the current state with the updated details. How do I go about doing that?
What I currently do is force a refetch from the server once the update request is successful to update the state again but I was wondering if there's a better way to do it without refetching from the server again.
For example, on the code below. I wanted to update PersonTwo's age.
state = {
users: [
{
name: PersonOne,
age: 1
},
{
name: PersonTwo,
age: 1
}
]
}
Let's say you have id field in your object. You can use map function to return new array and save it to your state.
updateUser(userId){
const updatedUsers = this.state.users.map( (user) => {
if(userId !== user.id) {
return user;
}
return {
...user,
//write some updated info here, for example:
age: 40
}
}):
this.setState({
users: updatedUsers
});
});
The best way is to do as you are doing right now that first send the values to the server and then fetch the latest data from the database because if you update the view before successful update on the server you might end up showing which does not exist actually. There is a possibility that your server did not accept that change but somehow you updated the view on user request. It is not a good thing, but if you still want to do that follow the steps below:
step-1: check the array index on which the update of user data is done and then
const newArray = Array.from(this.state.oldArray);
newArray[i] = 'test';
step-2: assign this new array to the old one:
this.setState({oldArray: newArray})
You can use functional setState as a success callback of POST/PUT call.
this.setState(prevState => {
users: ... // operation using prevState.users
})

React-select unexpected async behavior

There is an unexpected behavior with the react-selectcomponent.
I have it set up to load external json to populate the list.
<Select.Async
name="cf_search"
value=""
autoload={false}
cache={false}
ignoreAccents={false}
loadOptions={this.handleCFSearch}
onChange={this.handleSelectCFName}
/>
handleCFSearch = (input) => {
let term = encodeURIComponent(input);
return fetch(`${AppGlobal.baseBackend}/PersAddo/autocompleteSearch/${term}.json`)
.then((response) => {
if(response.ok) {
return response.json();
}
throw new Error('Network response was not ok.');
}).then((json) => {
console.log(json);
let values = json.result.map((element) => {
return {
value: element.pers_id,
label: element.first_name + ' ' + element.last_name
}
});
return { options: values };
});
}
The server script handles the search term and returns an array with the names in JSON.
It works just fine but in some cases it doesn't work.
If I search 'morten twe' the result from the server shows up in the select.
However if I search 'morte twe' (just one character less in the first name) the list does not show up and the select box just looks like not results.
I have tested both search terms and they both return exactly the same JSON:
{
"result": [
{
"pers_id": 123456,
"first_name": "Morten",
"last_name": "Twellmann"
}
]
}
So why doesn't anything show up when the server returns the data correctly?
react-select does some client-side filtering by default. So even though your API is returning the value, react-select filters it out. The default filter implementation it uses can be found here . You can see the filterOptions prop being called here in Select.js, which Async utilizes.
The default filtering is basically just checking for substring, case-insensitive equality. In the case where the value is not shown, there is a difference between the search and the result, so it is filtered out.
You can provide your own filterOptions function to override the default, or just pass undefined to turn off the clientside filtering altogether.

Resources