Array is empty after for loop - arrays

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.

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...

Angular/Firestore Collection Document Query to return a single document field from all documents into an array

I am performing a query on my collection documents and trying to return just all phone numbers into an array. I just want to set the phone numbers into array for use by another function. Firebase docs only show a console log for (doc.id) and (doc.data) and no practical use for any other objects in your documents. My console log for info.phoneNumbers returns all the phoneNumbers.
async getPhone() {
await this.afs.collection('members', ref => ref.where('phoneNumber', '>=', 0))
.get().toPromise()
.then(snapshot => {
if (snapshot.empty) {
console.log('No Matches');
return;
}
this.getInfo(snapshot.docs);
});
}
getInfo(data) {
data.forEach(doc => {
let info = doc.data();
console.log(info.phoneNumber, 'Phonenumbers');
// let myArray = [];
// myArray.push(doc.doc.data());
// const phoneNumber = info.phoneNumber as [];
// console.log(myArray, 'ARRAY');
return info.phoneNumber;
})
}```
Firestore is a "document store database". You fetch and store entire DOCUMENTS (think "JSON objects") at a time. One of the "anti-patterns" when using document store databases is thinking of them in SQL/relational DB terms. In SQL/relational DB, you "normalize" data. But in a document store database (a "NoSQL" database) we explicitly denormalize data -- that is, we duplicate data -- across documents on write operations. This way, when you fetch a document, it has all the data you need for its use cases. You typically want to avoid "JOINs" and limit the number of references/keys in your data model.
What you are showing in the code above is valid in terms of fetching documents, and extracting the phoneNumber field from each. However, use of .forEach() is likely not what you want. forEach() iterates over the given array and runs a function, but the return value of forEach() is undefined. So the return info.phoneNumber in your code is not actually doing anything.
You might instead use .map() where the return value of the map() function is a new array, containing one entry for each entry of the original array, and the value of that new array is the return value from map()'s callback parameter.
Also, mixing await and .then()/.catch() is usually not a good idea. It typically leads to unexpected outcomes. I try to use await and try/catch, and avoid .then()/.catch() as much as possible.
So I would go with something like:
try {
let querySnap = await this.afs.collection('members', ref =>
ref.where('phoneNumber', '>=', 0)).get();
let phoneNumbers = await this.getInfo(querySnap.docs[i].data());
} catch(ex) {
console.error(`EXCEPTION: ${ex.message}`);
}
getInfo(querySnapDocs) {
let arrayPhoneNumbers = querySnapDocs.map(docSnap => {
let info = doc.data();
let thePhoneNum = info.phoneNumber
console.log(`thePhoneNum is: ${thePhoneNum}`);
return thePhoneNum;
});
return arrayPhoneNumbers;
});
I solved this with help and I hope this may be helpful to others in Getting access to 1 particular field in your documents. In my service:
async getPhone() {
return await this.afs.collection('members', ref => ref.where('phoneNumber', '>=', 0))
.get().toPromise()
.then(snapshot => {
if (snapshot.empty) {
console.log('No Matches');
return;
}
return this.getInfoNum(snapshot.docs);
});
}
getInfoNum(data) {
return data.map(doc => {
let info = doc.data();
return info.phoneNumber
});
}
In my Component using typescript
phoneNumbers: string[] = [];
getPhone() {
this.dbService.getPhone().then(phoneNumbers => {
this.phoneNumbers = phoneNumbers;
this.smsGroupForm.controls.number.setValue(phoneNumbers.join(',')) //sets array seperated by commas
console.log(phoneNumbers);
});
}
This returns all the phone numbers in a comma separated array.
In my template I pull the numbers into an input for another function to send multiple text. Code in the template is not polished yet for the form, I am just getting it there for now.
<ion-list>
<ion-item *ngFor="let phoneNumber of phoneNumbers">
<ion-label position="floating">Phone Number</ion-label>
<ion-input inputmode="number"
placeholder="Phone Number"
formControlName="number"
type="number">{{ phoneNumber }}
</ion-input>
</ion-item>
</ion-list>

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
) ...

Defining dynamic Array in Typescript?

I have a requirement where i want to read a particular value x(that is auto-generated everytime) in a loop of say n times. Now, i want to store these autogenerated values of x, so, that i can later use them and iterate over it to perform my tests(protractor).
The way, i am trying to do is by creating an Array, using let list: string[] = [];. Now, i am pushing the values to my defined list using, list.push[x]; in each iteration. By the end of loop expecting to get the resulting Array having n values of x(string) in my list array. In order to validate, i did console.log(list); in each iteration and i can see that these values are being pushed in the defined list.
Later, in my code if i am trying to access these elements using let item = list[0]; i am getting the undefined value.
I think i need to initialize the Array to some particular size having default values initially and then modify them later in the loop. But, being new to TypeScript i am not able to find a solution on how to do it. Please help, TIA!!
Here, is the snippet below :
const tests = [
{type: 'admin', id='', uname='foo', pass='bar'},
{type: 'super', id='', uname='foo1', pass='bar'},
{type: 'normal', id='customId', uname='foo', pass='bar'}
];
let list: string[] = [];
// let list = [ //this is the final list that i got from the console.log(list);
// 'QR417msytVrq',
// 'V0fxayA3FOBD',
// 'QnaiegiVoYhs'];
describe(`Open Page `, () => {
//Code to get to the page
beforeAll(async () => {
//initialize page objects
});
describe(`Login User `, async () => {
tests.forEach(test => {
it(` should login user with `+test.type, async () => {
//....
//....
// On Success
const myId = userPage.getUID().getText();
list.push(myId);
console.log(list);
console.log(list.length);
});
});
});
describe(`Delete User`, async () => {
// describe(`Confirmation `, async () => {
console.log(list);
// list.forEach(item => { //this code doesn't gets executed and wasn't giving any error, so, commented out and tried to access the first element which is undefined.
let item = list[0];
console.log(item); //getting undefined value here.
it(` should select and Delete the User having id as ` + item, async () => {
//code to remove the user having id as item.
});
// });
});
});
Options to test deleting a user:
Ultimately, it's bad practice to make tests dependent on other tests.
That said, two or possibly three options which should work:
A: Iterate through list of users within one test
describe(`Delete User`, async () => {
describe(`Confirmation `, () => {
it(`Log all users out who previously logged in`, async () => {
list.forEach((item) => {
console.log(item);
});
});
});
});
Since the list array is populated by the previous test, inserting the code dependent on it inside of the next test would ensure that it has values to work with.
B: Login and delete user in one test
describe(`Login and delete user `, async () => {
tests.forEach(test => {
it(` should login and delete user with ` + test.type, async () => {
const myId = userPage.getUID().getText();
// Select and delete myId here
});
});
});
You may be able to remove the list array entirely by putting the entirety of the user flow into one large integration test.
C: Use mock data (may not be applicable if data is random)
describe(`Delete User`, async () => {
const list = ["QR417msytVrq", "V0fxayA3FOBD", "QnaiegiVoYhs"];
describe(`Confirmation `, () => {
list.forEach((item) => {
it(
` should select and Delete the User having id as ` + item,
async () => {}
);
});
});
});
If you know what the values to delete are going to be ahead of time, you can add them in manually. If the values are randomly generated, this won't work.
Other problems:
Testing order of execution
The dynamic array syntax you're using looks right however you appear to have an order of execution problem in your tests.
The code in the describe functions that is outside of the specs (the it blocks) is executed before any of the code inside of the specs is. The testing framework will traverse the tree of describe blocks, executing any code it finds but only taking note of the it specs. When it's finished this, it then executes the it specs it found in sequential order.
When you attempt to save the value of list[0], the 'Login User' specs have yet to be executed. More specifically:
describe(`Login User `, async () => {
tests.forEach(test => {
it(` should login user with ` + test.type, async () => {
// This code is executed AFTER the code in the 'Delete User'
// block but BEFORE the 'Delete User' spec
const myId = userPage.getUID().getText();
list.push(myId);
});
});
});
describe(`Delete User`, async () => {
// This code is executed before any specs are run
let item = list[0];
// List is [] when item is initialized
// The following spec will therefore not work as item is undefined
it(` should select and Delete the User having id as ` + item, async () => {
});
});
A possible solution to this would be to change the string of the 'Delete User' spec to something like ' should select and Delete first User' as well as move all the code outside of the spec to inside.
Describe blocks should not return promises
Your code sample has describe blocks (specifically 'Login User', 'Delete User', and 'Confirmation') which return Promises. You should remove the async in front of the function declarations. The specs can and should remain the same. For example:
describe(`Login User `, () => {
Object syntax
The tests object at the start of your sample isn't using JS/TS object syntax. Each key should be followed by a colon before the value instead of an equals sign. You likely meant to write:
const tests = [{
type: 'admin',
id: '',
uname: 'foo',
pass: 'bar'
},
{
type: 'super',
id: '',
uname: 'foo1',
pass: 'bar'
},
{
type: 'normal',
id: 'customId',
uname: 'foo',
pass: 'bar'
}
];
Sources:
Jest docs describing order of execution
SO on Promise returning in describe blocks

Array empty even after a loop

I'm trying to fill an array with some registers from a database. However, even after retrieving said registers and place them into the array, after the loop the array stays empty
const userParties = [];
userFollowing.forEach(element => {
// Here's when I gather the information that I need
dispatchGetUserEvents(element.user, response => {
userParties.push(response);
console.log(userParties); // Here the array is full of elements, like it's supposed to be
});
});
console.log(userParties); // However, here 'userParties' return '[]'
this.setState({ followUsersEvents: userParties });
this.setState({ userImage: userImg });
I tried to update the state array on the loop, but I had no luck there either.
'userParties' is not a state array btw.
const userParties = [];
userFollowing.forEach(element => {
// Here's when I gather the information that I need
dispatchGetUserEvents(element.user, response => {
userParties.push(response);
console.log('dispatchGetUserEvents', userParties); // Here the array is full of elements, like it's supposed to be
});
});
console.log('outside', userParties); // However, here 'userParties' return '[]'
this.setState({ followUsersEvents: userParties });
this.setState({ userImage: userImg });
run this code and you will see that outside will be printed first and dispatchGetUserEvents later, as I mentioned in comments dispatchGetUserEvents is async function, so first will be executed console.log('outside', ...);

Resources