How to load multiple data via service and wait for it in Angular2 - sqlite

I use Ionic 2 with Angular 2 in my project. In the root component you can click a "Add" button to add a new Report via a complex form and a lot of preprovided data (there are some selects that are feeded with data fetched from sqlite database)
Now in my "CreateReportComponent" i have the following constructor to load the data and assign it to local array variable:
selectEmployeeOptions: Employee[];
constructor(private dbService: DatabaseService) {
dbService.getAllEmployees().then(employees => {
this.selectEmployeeOptions = employees;
});
// load more data like tasks etc.
});
But when I want to modify this data in my component, the array is empty. I tried to do it in ngOnInit() but this seems to be to early as well.
I want to to something like this, before the component gets displayed:
dbService.getAllEmployees().then(employees => {
this.selectEmployeeOptions = employees;
// modify data
this.selectEmployeeTitleOptions = employees.map((item) => {
return item.title;
});
console.log(JSON.stringify(this.selectEmployeeTitleOptions)) // --> empty
});
But selectEmployeeTitleOptions is empty...
The function in the databaseService looks like this:
getAllEmployees(): Promise<Emplyoee[]> {
let query = "SELECT * FROM employees";
let employeeList = [];
this.database.executeSql(query, []).then((data) => {
if(data.rows.length > 0) {
let e = new Employee();
e.id = data.rows.item(i).id;
e.firstname = data.rows.item(i).firstname;
e.lastname = data.rows.item(i).lastname;
employeeList.push(e);
}
}, (error) => {
// handle error
});
return Promise.resolve(employeeList);
}
I read that there is the Resolve pattern (https://blog.thoughtram.io/angular/2016/10/10/resolving-route-data-in-angular-2.html) But I need to make multiple calls and not only for contacts as in the example.
So the question: How to wait for multiple calls to database?

i think something go wrong here
getAllEmployees(): Promise<Emplyoee[]> {
let query = "SELECT * FROM employees";
let employeeList = [];
this.database.executeSql(query, []).then((data) => {
if(data.rows.length > 0) {
let e = new Employee();
e.id = data.rows.item(i).id;
e.firstname = data.rows.item(i).firstname;
e.lastname = data.rows.item(i).lastname;
employeeList.push(e);
}
}, (error) => {
// handle error
});
return Promise.resolve(employeeList);
}
first return Promise.resolve(employeeList); will return empty array, because it is async process.
you need loop through data.rows, then format return data like this.
getAllEmployees(): Promise<Employee[]> {
let query = "SELECT * FROM employees";
return this.database.executeSql(query, []).then((data) => {
let arr = [];
for(let i = ; i < data.rows.length; ++i) {
let emp = data.rows.item(i);
let e = new Employee();
e.id = emp.id;
e.firstname = emp.firstname;
e.lastname = emp.lastname;
arr.push(e);
}
return arr;
});
}
note that .then() return a promise object.

What you are looking for is forkJoin method that returns Observable that you should switch to instead of using Promises, for reference about why you should do this check here.
Short information about fork join from its GitHub page:
Runs all observable sequences in parallel and collect their last elements.
This way you can safely make parallel requests to your API.
For more information regarding forkJoin go here.
Additionally you should call services using ngOnInit as you mentioned before. For more information about Angular 2 lifecycle hooks see the docs.

You can use Promise.all
https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
You push all promises to an array, and then go
let foo : [Promise<Emplyoee[]>,Promise<void>] = [getAllEmployees(), method2()];
Promise.all(foo).then((results:any[]) => {
let employeearray: any = results[0];
/* and on an on */
});

Related

Firestore query "onSnapshot" called at the same time does not work (

I created an app with Ionic and Firestore that features live chat and I'm having a problem with it.
The conversation is loaded with the method:
refUneConversationMyUserCol.ref.orderBy('date', 'desc').limit(20).get()
To this is added an "onSnapshot" request to retrieve the last message sent live
this.unsubscribeDataUneConversation = refUneConversationMyUserCol.ref.orderBy('date', 'desc').limit(1).onSnapshot(result => {
console.log(result.docs[0].data());
if (this.isCalledBySnapshot === false) {
this.isCalledBySnapshot = true;
} else if (result.docs[0].data().expediteur !== this.authentificationService.uidUserActif) {
const data = result.docs[0].data();
const id = result.docs[0].id;
this.dataUneConversation.push({ id, ...data } as UneConversation);
}
});
It will work perfectly however, when I send a message at the same time (with 2 different accounts talking to each other), I encounter a problem, the onSnapshot is triggered only once and I only receive one message.
I specify that the two messages are sent well in the database, they are only not displayed both during the live session
Do you have any idea why?
Thank you
(Here is the whole method)
async getDataUneConversation(idI: string) {
if (this.loadedDataUneConversation !== idI) {
/* ANCHOR Msg en direct */
this.isCalledBySnapshot = false;
if (this.unsubscribeDataUneConversation) {
await this.unsubscribeDataUneConversation();
}
const refUneConversationMyUserCol = this.afs.collection<User>('users').doc<User>(this.authentificationService.uidUserActif).collection<Conversations>('conversations');
const result = await refUneConversationMyUserCol.ref.orderBy('date', 'desc').limit(20).get();
/* ANCHOR Msg en direct */
this.unsubscribeDataUneConversation = refUneConversationMyUserCol.ref.orderBy('date', 'desc').limit(1).onSnapshot(result => {
console.log(result.docs[0].data());
if (this.isCalledBySnapshot === false) {
this.isCalledBySnapshot = true;
} else if (result.docs[0].data().expediteur !== this.authentificationService.uidUserActif) {
const data = result.docs[0].data();
const id = result.docs[0].id;
this.dataUneConversation.push({ id, ...data } as UneConversation);
}
});
/* ANCHOR Msg en brut */
if (result.docs.length < 20) {
this.infiniteLastUneConversationMax = true;
} else {
this.infiniteLastUneConversationMax = false;
}
this.infiniteLastUneConversation = result.docs[result.docs.length - 1];
this.dataUneConversation = result.docs.map(doc => {
const data = doc.data();
const id = doc.id;
return { id, ...data } as UneConversation;
});
this.dataUneConversation.reverse();
this.loadedDataUneConversation = idI;
}
}
EDIT for working :
this.unsubscribeDataUneConversation = refUneConversationMyUserCol.ref.orderBy('date', 'asc').startAfter(this.dataUneConversation[this.dataUneConversation.length
- 1].date).onSnapshot(result => {
result.docs.forEach(element => {
const data = element.data();
const id = element.id;
if (!this.dataUneConversation.some(e => e.id === element.id)) {
this.dataUneConversation.push({ id, ...data } as UneConversation);
}
});
});
You're limiting live messages to only one last message. In a chat app, you want to listen to all new messages. So the issue is probably in your .limit(1) clause.
But if you do that, I understand that you'll get the whole conversation, with all messages, since the conversation started.
My approach would be like this:
Get the date of the last message from your refUneConversationMyUserCol... conversation loader.
When you do the onSnapshot() to get the last message, do not limit to 1 message, instead, start at a date after the date of the last loaded message.
Since you're ordering by date anyway, this will be an easy fix. Look into "Adding a cursor to your query".
Basically, you'll be saying to Firestore: give me LIVE new messages but start at NOW - and even if there are many messages posted at the same time, you'll get them all, since you're not limiting to 1.
Feel free to ask if this is not clear enough.

Is there any way to get the data from node on "child_added" event using cloud-function of firebase?

I was using the query "OnUpdate" on each client to get the data from that node and calculate the children-count but it is too costly.
So I decided to use a cloud-function and create another node of children-count based on the node in which all the users exist but there is an issue, I'm unable to find any query like "OnChildAdded".
The available queries listed on firebase documentation are "OnUpdate", "OnDelete", "OnWrite" and "OnCreate" that are useless for this case because using "OnCreate" on child node cannot return me the children of parent node or "OnUpdate" on parent node will again become costly because all the users update their states frequently.
So what about "OnOperation"? Is there any use of it or is there any other way to reduce the cost of query and also create a children-count node?
Here is the structure of my database
{
currentGame: {
players: {
playerId: {...playerGameData},
//,
},
noOfPlayer: // this is what i wanted to create based on above players node children_count.
}
}
Here is the solution to the above problem in case anyone else need to solve a similar issue.
const PLAYER_REF = "currentGame/players/{playerId}";
const PLAYER_COUNT_NODE = "currentGame/noOfPlayers";
exports.incPlayersCount = functions.database.ref (PLAYER_REF).onCreate (async (snap) =>
{
const countRef = snap.ref.root.child (PLAYER_COUNT_NODE);
await countRef.transaction((current) => {
return (typeof current !== "number" || current < 0) ? 1 : current + 1;
});
return null;
});
exports.decPlayersCount = functions.database.ref (PLAYER_REF).onDelete (async (snap) =>
{
const countRef = snap.ref.root.child (PLAYER_COUNT_NODE);
await countRef.transaction((current) => {
return (typeof current !== "number" || current <= 0) ? 0 : current - 1;
});
return null;
});
btw - it is exactly similar to the sample code that #FrankvanPuffelen have shared in the above comments.

Async Await with four nested loops

I am currently trying to return a JSON object array that requires me to do one asynchronous function and then four nested asynchronous map functions in order to populate an array of entities. Basically, each user has an array of orders, each order has an array of items, each item has an array of options and each option has an array of values. I am using loopback4 framework and therefore cannot do res.send once all things have been populated. The function seems to return on the first await, but any await after that, it does not wait on it and instead runs to the end of the function. I have tried using Promises and .thens(), but cannot seem to figure out how to populate each entity fully nested, and then return the array of populated entities. I keep getting an empty array. Below is only one nest of maps, but I cannot get it to even populate up to the first nest and return this, so I decided not to go any further. This is the code:
async getUserOrders2(#param.path.number('id') id: number): Promise<any> {
if ( !this.user) {
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
}
else if (this.user.id != id) {
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
}
else {
let restaurantId = this.user.restaurantId
let orderFrameArray = new Array<OrderFrame>()
return this.restaurantRepository.orders(restaurantId as string).find()
.then(async orders => {
orders.map(async (val, key)=> {
let orderFrame = new OrderFrame(val)
orderFrame.itemArray = await this.orderRepository.orderItems(val.id).find()
orderFrameArray.push(orderFrame)
})
orderFrameArray = await Promise.all(orderFrameArray)
return orderFrameArray
})
}
}
The function is returning before the orderFrameArray has been populated. I need four nested map loops and this first one is not working, so I am not sure how to do the rest. Any help would be extremely appreciated.
Based on #Tomalaks solution I tried the following, but its still only returning the top level array and nothing is nested:
async getUserOrders2(#param.path.number('id') id: number): Promise<any> {
if ( !this.user) {
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
}
else if (this.user.id != id) {
throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
}
else {
let restaurantId = this.user.restaurantId
let orderFrameArray = new Array<OrderFrame>()
return this.restaurantRepository.orders(restaurantId as string).find()
.then(orders => {Promise.all(orders.map(
order => {
let orderFrame = new OrderFrame(order)
orderFrame.itemArray = new Array<Item>()
this.orderRepository.orderItems(order.id).find()
.then(orderItems => Promise.all(orderItems.map(
orderItem => {
let itemFrame = new Item(orderItem)
itemFrame.options = new Array<Option>()
this.orderItemRepository.orderItemOptions(orderItem.id).find()
.then(orderItemOptions => Promise.all(orderItemOptions.map(
orderItemOption => {
let optionFrame = new Option(orderItemOption)
optionFrame.values = new Array<Value>()
this.orderItemOptionRepository.orderItemOptionValues(orderItemOption.id).find()
.then(orderItemOptionValues => Promise.all(orderItemOptionValues.map(
orderItemOptionValue => {
let valueFrame = new Value(orderItemOptionValue)
optionFrame.values.push(valueFrame)})))
itemFrame.options.push(optionFrame)})))
orderFrame.itemArray.push(itemFrame)})))
orderFrameArray.push(orderFrame)}))
return orderFrameArray})
}
}
I apologize for the formatting I wasn't sure how best to format it. Is there something else I'm doing wrong?
Thanks to everyone for their response. The answer that was posted by #Tomalak was correct. I just had to surround the entire function in brackets, and put a .then to return the populated entity I had made
You only need to use async when you are using await in the same function. If there's await in a nested function, the parent function does not need async.
However, in your case, there is no function that should be made async in the first place.
There is no benefit in awaiting any results in your function, because no code inside depends on any intermediary result. Just return the promises as you get them.
There's no need for intermediary result variables like orderFrameArray, you're making things harder than they are with your approach of awaiting individual orders and pushing them to a top-level variable.
Using await in a loop like you do inside your .map() call is bad for performance. You are basically serializing database access this way – the next query will only be sent after the current one has returned. This kind of daisy-chaining nullifies the database's ability to process multiple concurrent requests.
getUserOrders2 is not Promise<any>, it's Promise<Array<OrderFrame>>.
throw terminates the function anyway, you can do multiple checks for error conditions without using else if. This reduces nesting.
So a fully asynchronous function would look like this:
getUserOrders2(#param.path.number('id') id: number): Promise<Array<OrderFrame>> {
if (!this.user) throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
if (this.user.id != id) throw new HttpErrors.Unauthorized(AuthErrorKeys.ClientInvalid);
return this.restaurantRepository
.orders(this.user.restaurantId).find().then(
orders => Promise.all(orders.map(
order => this.orderRepository.orderItems(order.id).find().then(
order => new OrderFrame(order)
)
))
);
}
The async/await equivalent of this function would be more complex.
You then would await the result in the calling code, as you would have to do anyway:
async test() {
const orders = await foo.getUserOrders2(someUserId);
// ...
}
// or
test() {
foo.getUserOrders2(someUserId).then(orders => {
// ...
});
}

Using promises in a javascript filter function and returning it

I am trying to return an array of everyone session (me) is not following with JavaScript's filter function with the help of a promise and sending it as a JSON response.
But it doesn't work.
Thanks in advance!!
app.get('/explore', (req, res) => {
P.coroutine(function *(){
let
{ id: session } = req.session,
followings = yield db.query('SELECT id, username, email FROM users WHERE id <> ? ORDER BY RAND() LIMIT 10', [session]),
d = followings.filter(e => {
db.is_following(session, e.id).then(s => s ) // returns boolean
})
res.json(d)
})()
})
Array.prototype.filter is synchronous - you can't filter an array with an asynchronous filter.
What you can do, is create an array of Promises and then when all of them are resolved, return the response:
var promises = [];
var d = [];
followings.forEach(function(e) {
promises.push(
db.is_following(session,e.id).then(function(s) {
//following, push e onto `d`
d.push(e);
}).catch(function() {
//not following, I assume, do nothing
})
);
});
Promise.all(promises).then(function() {
//send the response after all the is_following requests have finished
res.json(d);
});
Adam's solution worked very well, but I have found another solution which uses async/await.
Code is much less and human-readable!!
app.post('/explore', async function(req, res) {
let
{ id: session } = req.session,
exp = [],
followings = await db.query(
'SELECT id, username, email FROM users WHERE id <> ? ORDER BY RAND() LIMIT 10',
[session]
)
for (let f of followings) {
let is = await db.is_following(session, f.id)
!is ? exp.push(f) : null
}
res.json(exp)
})

In Meteor, how to choose a collection based on a variable?

Let's say you want to dynamically insert into different collections. Right now I am using a switch statement:
switch (i) {
case "dog":
Dog.insert({
name: "Skippy"
});
break;
case "cat":
Cat.insert({
name: "Skippy"
});
break;
}
But this is messy, and if I need to support future collections, it fails. Is there a way to choose the collection based on "i" in the example above?
Correct me if I am wrong but I think this is what you are trying to do:
var Dog = {
insert: function(props) {
console.log(props);
}
}
var insertArbitraryDocument = (function(collectionType, props) {
window[collectionType].insert(props)
}).bind(this);
insertArbitraryDocument('Dog', {name: 'skippy'}); //=> {name: 'skippy'}
In this snippet you are accessing the window object and getting the property of whatever name you are passing in (must be exactly the same as the collection). Then you can call your usual function calls.
I don't think there is a meteor built-in way of doing this, but it's pretty easy to just create a directory of collections manually:
JS in common to client and server:
var collections = {};
function myColl(name) {
var coll = new Meteor.Collection(name);
collections[name] = coll;
return coll;
}
// and now just use myColl instead of new Meteor.Collection
Dog = myColl('dog');
And then, to do what you want to do:
collections[i].insert(data);
Here's a complete working example:
Posts = new Mongo.Collection('posts');
Comments = new Mongo.Collection('comments');
var capitalize = function(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
};
var nameToCollection = function(name) {
// pluralize and capitalize name, then find it on the global object
// 'post' -> global['Posts'] (server)
// 'post' -> window['Posts'] (client)
var root = Meteor.isClient ? window : global;
return root[capitalize(name) + 's'];
};
var insertSomething = function(name, data) {
var collection = nameToCollection(name);
collection.insert(data);
}
Meteor.startup(function() {
// ensure all old documents are removed
Posts.remove({});
Comments.remove({});
// insert some new documents
insertSomething('post', {message: 'this a post'});
insertSomething('comment', {message: 'this a comment'});
// check that it worked
console.log(Posts.findOne());
console.log(Comments.findOne());
});
Note this is nearly identical to this question but I simplified the answer for more generic use.

Resources