I have been working around this for a really long while and I think I'm sort of giving up and coming here to ask now. I have a cloud code function that first lets me get a list of categories. Every category has multiple products. This one-to-many relationship is carried out using pointers, where every product points to the category it belongs to. However I'm failing to retrieve any products.
This my code,
Parse.Cloud.define("GetCategories", function(request, response) {
var _ = require('underscore.js')
var MSTCATEGORY_CLASS = "MSTCategory";
var MSTPRODUCT_CLASS = "MSTProduct";
var CATEGORY_TAG = "Category";
var SHOP_TAG = "Shop";
var VIEWS_TAG = "Views";
var CAT_LIMIT = 10;
var PROD_LIMIT = 5;
var productList = [];
var CatAndProd = [];
var MSTCategory = Parse.Object.extend(MSTCATEGORY_CLASS);
var query = new Parse.Query(MSTCategory);
query.limit(CAT_LIMIT);
query.find().then(function(results) {
var promise = Parse.Promise.as();
_.each(results, function(result) {
promise = promise.then(function() {
var MSTProduct = Parse.Object.extend(MSTPRODUCT_CLASS);
var ProdQuery = new Parse.Query(MSTProduct);
ProdQuery.equalTo(CATEGORY_TAG, result);
ProdQuery.include(CATEGORY_TAG);
ProdQuery.include(SHOP_TAG);
ProdQuery.descending(VIEWS_TAG);
ProdQuery.limit(PROD_LIMIT);
var category = result;
ProdQuery.find().then(function(ProdResults){
ProdResults.forEach(function(product) {
productList.push(product);
});
var singleItem = {
"category" : category,
"products" : productList
};
CatAndProd.push(singleItem);
return Parse.Promise.as("Hello!");
});
});
});
return promise;
}).then(function(hello) {
var jsonObject = {
"categoryAndProducts": CatAndProd
};
response.success(jsonObject);
});
});
What I am trying to do is after getting the category, I'd fetch products in it, add them to a jsonObject. And once I'm done with all categories. I'll create an array to carry all those json objects and send it as response. This is really basic, I'm pretty sure it's become my logic is incorrect. I'm new to Javascript and Parse.
There are few errors:
in your _.each, you are overwriting the same promise object in each loop
you can return directly your singleItem via the promise resolving, not need for an extra array
you're using a global productList array so the last category will have ALL products.
Try like this, I put few comments in the code:
query.find().then(function(results) {
// an array to store all our promises
var promises = [];
results.forEach(function(result) {
// one promise for each result
var promise = new Parse.Promise();
var MSTProduct = Parse.Object.extend(MSTPRODUCT_CLASS);
var ProdQuery = new Parse.Query(MSTProduct);
ProdQuery.equalTo(CATEGORY_TAG, result);
ProdQuery.include(CATEGORY_TAG);
ProdQuery.include(SHOP_TAG);
ProdQuery.descending(VIEWS_TAG);
ProdQuery.limit(PROD_LIMIT);
var category = result;
ProdQuery.find().then(function(ProdResults) {
// remove this
// ProdResults.forEach(function(product) {
// productList.push(product);
// });
var singleItem = {
"category" : category,
"products" : ProdResults
};
// pass singleItem directly when resolving the promises
promise.resolve(singleItem);
});
promises.push(promise);
});
// return a promise resolving when ALL Promises are resolved
return Parse.Promise.when(promises);
}).then(function(allProducts) {
// allProducts is an array of singleItem
var jsonObject = {
"categoryAndProducts": allProducts
};
response.success(jsonObject);
});
Also, try to not mix Array.forEach and _.each, use one of them but not both at the same time.
Related
I'm working with mongodb stitch/realm and I'm trying to modify objects inside an array with a foreach and also pushing ids into a new array.
For each object that i'm modifying, I'm also doing a query first, after the document is found I start modifying the object and then pushing the id into another array so I can use both arrays later.
The code is something like this:
exports = function(orgLoc_id, data){
var HttpStatus = require('http-status-codes');
// Access DB
const db_name = context.values.get("database").name;
const db = context.services.get("mongodb-atlas").db(db_name);
const orgLocPickupPointCollection = db.collection("organizations.pickup_points");
const orgLocStreamsCollection = db.collection("organizations.streams");
const streamsCollection = db.collection("streams");
let stream_ids = [];
data.forEach(function(stream) {
return streamsCollection.findOne({_id: stream.stream_id}, {type: 1, sizes: 1}).then(res => { //if I comment this query it will push without any problem
if(res) {
let newId = new BSON.ObjectId();
stream._id = newId;
stream.location_id = orgLoc_id;
stream.stream_type = res.type;
stream.unit_price = res.sizes[0].unit_price_dropoff;
stream._created = new Date();
stream._modified = new Date();
stream._active = true;
stream_ids.push(newId);
}
})
})
console.log('stream ids: ' + stream_ids);
//TODO
};
But when I try to log 'stream_ids' it's empty and nothing is shown. Properties stream_type and unit_price are not assigned.
I've tried promises but I haven't had success
It's an asynchronous issue. You're populating the value of the array inside a callback. But because of the nature of the event loop, it's impossible that any of the callbacks will have been called by the time the console.log is executed.
You mentioned a solution involving promises, and that's probably the right tack. For example something like the following:
exports = function(orgLoc_id, data) {
// ...
let stream_ids = [];
const promises = data.map(function(stream) {
return streamsCollection.findOne({ _id: stream.stream_id }, { type: 1, sizes: 1 })
.then(res => { //if I comment this query it will push without any problem
if (res) {
let newId = new BSON.ObjectId();
// ...
stream_ids.push(newId);
}
})
})
Promise.all(promises).then(function() {
console.log('stream ids: ' + stream_ids);
//TODO
// any code that needs access to stream_ids should be in here...
});
};
Note the change of forEach to map...that way you're getting an array of all the Promises (I'm assuming your findOne is returning a promise because of the .then).
Then you use a Promise.all to wait for all the promises to resolve, and then you should have your array.
Side note: A more elegant solution would involve returning newId inside your .then. In that case Promise.all will actually resolve with an array of the results of all the promises, which would be the values of newId.
I have been trying to solve this issue for a long long time but unable to get it working.
g_globalList.once("value").then(function(tickList){
var multiPaths = [];
tickList.forEach(function(ticker){
ticker.val().forEach(function(path){
multiPaths.push(path);
});
});
return multiPaths;
}).then(function(multiPaths){
var myObj = {};
multiPaths.forEach(function(path){
var ticker = path.substr(path.lastIndexOf("/")+1, 40);
console.log("adding " + ticker);
****this is another promise and make my "myObj" to print blank****
db.child("symbols/NSE/" +ticker).once('value').then(function(data){
if(data.exists()){
myObj[path] = data.val();
}
});
});
return myObj;
}).then(function(myObj){
console.log(myObj);
});
Is there anyway, I can call the last time "console.log(myObj)" after all promises are completed ?
You can use Promise#all for wait all Promises resolve, check the example bellow (i not tested but maybe will give you an idea to resolve your problem):
var allPaths = [];
g_globalList.once("value").then(function(tickList){
return Promise.all(
//map will create a array of the returned values (Promises in this case)
tickList.map(function(ticker){
return ticker.val();
})
);
}).then(function(values){//values represents all then results
allPaths = values; //store for later all paths
return Promise.all(
//map will create a array of returned values (Promises in this case)
values.map(function(path){
var ticker = path.substr(path.lastIndexOf("/")+1, 40);
return db.child("symbols/NSE/" +ticker).once('value');
})
);
}).then(function(values){//values represents all then results
var myObj = {}; //now lets create a obj
for(var i = 0; i < allPaths.length; i++){ //foreach path
var path = allPaths[i];
//get the ticker name again
var ticker = path.substr(path.lastIndexOf("/")+1, 40);
//check your data
var data = values[i];
if(data.exists()){
//add to the obj
myObj[path] = data.val();
}
}
//return the obj
return myObj;
})
.then(function(myObj){
//i hope its here now
console.log(myObj);
});
Use map on multiPaths instead of forEach. Return each db.child call and wrap the map call with a Promise.all() and return that too.
I've been trying to code up a search engine using angular js, but I can't copy one array to another. When I initiate the the code (in the service.FoundItems in the q.all then function) new array(foundArray) shows up as an empty array. I searched up how to copy one array to another and tried that method as you can see, but it isn't working. Please help, here is the code, and thank you.
P.S. if you need the html please tell me.
(function () {
'use strict';
angular.module('narrowDownMenuApp', [])
.controller('narrowItDownController', narrowItDownController)
.service('MenuSearchService', MenuSearchService)
.directive('searchResult', searchResultDirective);
function searchResultDirective() {
var ddo = {
templateUrl: 'searchResult.html',
scope: {
items: '<'
},
};
return ddo
}
narrowItDownController.$inject = ['MenuSearchService'];
function narrowItDownController(MenuSearchService) {
var menu = this;
menu.input = "";
menu.displayResult = [];
menu.searchX = function(name) {
menu.displayResult = MenuSearchService.FoundItems(menu.input, name);
console.log(menu.displayResult);
};
}
MenuSearchService.$inject = ['$http', '$q'];
function MenuSearchService($http, $q) {
var service = this;
service.getMatchedMenuItems = function(name, searchTerm) {
var deferred = $q.defer();
var foundItems = [];
var result = $http({
method: "GET",
url: ('https://davids-restaurant.herokuapp.com/menu_items.json'),
params: {
category: name
}
}).then(function (result) {
var items = result.data;
for (var i = 0; i < items.menu_items.length; i++) {
if (searchTerm === ""){
deferred.reject("Please enter search term");
i = items.menu_items.length;
}
else if (items.menu_items[i].name.toLowerCase().indexOf(searchTerm.toLowerCase()) ==! -1){
foundItems.push(items.menu_items[i].name)
deferred.resolve(foundItems);
}else {
console.log("doesn't match search");
}
}
});
return deferred.promise;
};
service.FoundItems = function (searchTerm, name) {
var searchResult = service.getMatchedMenuItems(name, searchTerm);
var foundArray = [];
$q.all([searchResult])
.then(function (foundItems) {
foundArray = foundItems[0].slice(0);
foundArray.reverse();
})
.catch(function (errorResponse) {
foundArray.push(errorResponse);
});
console.log(foundArray);
return foundArray;
};
};
})();
If the goal of the service.FoundItems function is to return a reference to an array that is later populated with results from the server, use angular.copy to copy the new array from the server to the existing array:
service.FoundItems = function (searchTerm, name) {
var foundArray = [];
var searchPromise = service.getMatchedMenuItems(name, searchTerm);
foundArray.$promise = searchPromise
.then(function (foundItems) {
angular.copy(foundItems, foundArray);
foundArray.reverse();
return foundArray;
})
.catch(function (errorResponse) {
return $q.reject(errorResponse);
})
.finally(function() {
console.log(foundArray);
});
return foundArray;
};
I recommend that the promise be attached to the array reference as a property named $promise so that it can be used to chain functions that depend on results from the server.
Frankly I don't recommend designing services that return array references that are later populated with results. If you insist on designing it that way, this is how it is done.
I tried the $promise thing that you recommended. I was wondering how you would get the value from it ie the array.
In the controller, use the .then method of the $promise to see the final value of the array:
narrowItDownController.$inject = ['MenuSearchService'];
function narrowItDownController(MenuSearchService) {
var menu = this;
menu.input = "";
menu.displayResult = [];
menu.searchX = function(name) {
menu.displayResult = MenuSearchService.FoundItems(menu.input, name);
̶c̶o̶n̶s̶o̶l̶e̶.̶l̶o̶g̶(̶m̶e̶n̶u̶.̶d̶i̶s̶p̶l̶a̶y̶R̶e̶s̶u̶l̶t̶)̶;̶
menu.displayResult.$promise
.then(function(foundArray) {
console.log(foundArray);
console.log(menu.displayResult);
}).catch(function(errorResponse) {
console.log("ERROR");
console.log(errorResponse);
});
};
}
To see the final result, the console.log needs to be moved inside the .then block of the promise.
Titus is right. The function always immediately returns the initial value of foundArray which is an empty array. The promise is executed asynchronously so by the time you are trying to change foundArray it is too late. You need to return the promise itself and then using .then() to retrieve the value just like you are currently doing inside the method.
From just quickly looking at your code I think you made have a simple error in there. Are you sure you want
foundArray = foundItems[0].slice(0);
instead of
foundArray = foundItems.slice(0);
I was having some problem with multi-level of promises. What I tried to do is first get list of receipt items under certain category, then for each receipt item, I get its detail & receipt ID, after I get the receipt ID, I search for the account ID. Then, I get the account details based on account ID. Here is my code:
var query = // get receipt items under certain category
var outerPromise = query.once('value').then(data => {
var promises = [];
var datasetarr = [];
data.forEach(snapshot => {
var itemData = // get receipt item unique push ID
var query = // get details of receipt items
var promise = query.once('value').then(data => {
var itemDetail = // get receipt item detail
if(type == subtype){
var receiptID = itemDetail.receiptID;
var query = // query receipts table by receiptID
return query.once('value').then(data => {
data.forEach(snapshot => {
snapshot.forEach(childSnapshot => {
if(childSnapshot.key == receiptID){
var accountKey = // get accountID
var query = // query accounts table
return query.once('value').then(data => {
var accountDetail = data.val();
var age = accountDetail.age;
var gender = accountDetail.gender;
console.log(age + ' ' + gender);
datasetarr.push({age: age, gender: gender});
});
}
});
});
});
}
});
promises.push(promise);
});
return Promise.all(promises).then(()=> datasetarr);
});
I managed to print out the result from the console.log above. However, when I tried to print out here which is when the promise is done:
outerPromise.then((arr) => {
console.log('promise done');
for(var i = 0; i < arr.length; i++){
console.log(arr[i].age + ' ' + arr[i].gender);
}
});
I get nothing here. The console now is showing 'promise done' first before any other results I printed out above.
How can I do this correctly?
I will provide a more detailed explanation in a couple of hours, I have a prior engagement which means I can't provide details now
First step to a "easy" solution is to make a function to make an array out of a firebase snapshot, so we can use map/concat/filter etc
const snapshotToArray = snapshot => {
const ret = [];
snapshot.forEach(childSnapshot => {
ret.push(childSnapshot);
});
return ret;
};
Now, the code can be written as follows
// get list of receipt items under category
var query // = // get receipt items under certain category
var outerPromise = query.once('value').then(data => {
return Promise.all(snapshotToArray(data).map(snapshot => {
var itemData // = // get receipt item unique push ID
var query // = // get details of receipt items
return query.once('value').then(data => {
var itemDetail // = // get receipt item detail
if(type == subtype){
var receiptID = itemDetail.receiptID;
var query //= // query receipts table by receiptID
return query.once('value').then(data => {
return Promise.all([].concat(...snapshotToArray(data).map(snapshot => {
return snapshotToArray(snapshot).map(childSnapshot => {
if(childSnapshot.key == receiptID){
var accountKey //= // get accountID
var query //= // query accounts table
return query.once('value').then(data => {
var accountDetail = data.val();
var age = accountDetail.age;
var gender = accountDetail.gender;
console.log(age + ' ' + gender);
return({age, gender});
});
}
}).filter(result => !!result);
}).filter(result => !!result)));
});
}
});
})).then([].concat(...results => results.filter(result => !!result)));
});
To explain questions in the comments
[].concat used to add the content of multiple arrays to a new array, i.e
[].concat([1,2,3],[4,5,6]) => [1,2,3,4,5,6]
...snapshotToArray(data).map(etc
... is the spread operator, used as an argument to a function, it takes the iterable and "spreads" it to multiple arguments
console.log(...[1,2,3]) == console.log(1,2,3)
In this case snapshotToArray(data).map returns an array of arrays, to give a console log example
console.log(...[[1,2],[3,4]]) == console.log([1,2], [3,4])
adding the concat
[].concat(...[[1,2],[3,4]]) == [].concat([1,2],[3,4]) == [1,2,3,4]
so it flattens a two level array to a single level, i.e.
console.log(...[[1,2],[3,4]]) == console.log(1,2,3,4)
So in summary, what that code fragment does is flatten a two level array
filter(result => !!result)
simply "filters" out any array elements that are "falsey". As you have this condition
if(childSnapshot.key == receiptID){
if that is false, the result will be undefined for that map - all other results will be an array, and even empty arrays are truthy - that's why the filtering is done so often! There's probably a better way to do all that, but unless you're dealing with literally millions of items, there's no real issue with filtering empty results like this
End result is a flat array with only the Promises returned from the code within
I am writing some JavaScript codes using Parse.com.
To be honest, I have been reading how to use Promise and done lots of research but cannot still figure out how to use it properly..
Here is a scenario:
I have two tables (objects) called Client and InvoiceHeader
Client can have multiple InvoiceHeaders.
InvoiceHeader has a column called "Amount" and I want a total amount of each client's InvoiceHeaders.
For example, if Client A has two InvoiceHeaders with amount 30 and 20 and Client B has got nothing, the result I want to see in tempArray is '50, 0'.
However, with the following codes, it looks like it's random. I mean sometimes the tempArray got '50, 50' or "50, 0". I suspect it is due to the wrong usage of Promise.
Please help me. I have been looking into the codes and stuck for a few days.
$(document).ready(function() {
var client = Parse.Object.extend("Client");
var query = new Parse.Query(client);
var tempArray = [];
query.find().then(function(objects) {
return objects;
}).then(function (objects) {
var promises = [];
var totalForHeader = 0;
objects.forEach(function(object) {
totalForHeader = 0;
var invoiceHeader = Parse.Object.extend('InvoiceHeader');
var queryForInvoiceHeader = new Parse.Query(invoiceHeader);
queryForInvoiceHeader.equalTo('headerClient', object);
var prom = queryForInvoiceHeader.find().then(function(headers) {
headers.forEach(function(header) {
totalForHeader += totalForHeader +
parseFloat(header.get('headerOutstandingAmount'));
});
tempArray.push(totalForHeader);
});
promises.push(prom);
});
return Parse.Promise.when.apply(Parse.Promise, promises);
}).then(function () {
// after all of above jobs are done, do something here...
});
} );
Assuming Parse.com's Promise class follows the A+ spec, and I understood which bits you wanted to end up where, this ought to work:
$(document).ready(function() {
var clientClass = Parse.Object.extend("Client");
var clientQuery = new Parse.Query(clientClass);
clientQuery.find().then(function(clients) {
var totalPromises = [];
clients.forEach(function(client) {
var invoiceHeaderClass = Parse.Object.extend('InvoiceHeader');
var invoiceHeaderQuery = new Parse.Query(invoiceHeaderClass);
invoiceHeaderQuery.equalTo('headerClient', client);
var totalPromise = invoiceHeaderQuery.find().then(function(invoiceHeaders) {
var totalForHeader = 0;
invoiceHeaders.forEach(function(invoiceHeader) {
totalForHeader += parseFloat(invoiceHeader.get('headerOutstandingAmount'));
});
return totalForHeader;
});
totalPromises.push(totalPromise);
});
return Parse.Promise.when(totalPromises);
}).then(function(totals) {
// here you can use the `totals` array.
});
});