I have the following code set up as a job in the Parse Cloud Code for my application.
Parse.Cloud.job("requestLocations", function (request, response) {Parse.Cloud.httpRequest({
url: 'https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=29.7030428,-98.1364808&radius=900&types=restaurant&key=AIzaSyCTg0x68Q6lrCAo6-A37zkxge81jDEKpvo'
}).then(function (httpResponse) {
// Success
response.success("Success");
var parsedData = JSON.parse(httpResponse.text);
var Location = Parse.Object.extend("Locations");
for (var i = 0; i < parsedData.results.length; i++) {
var restaurant = new Location();
var placeId = parsedData.results[i].place_id;
var name = parsedData.results[i].name;
var vicinity = parsedData.results[i].vicinity;
var point = new Parse.GeoPoint({
latitude: parsedData.results[i].geometry.location.lat,
longitude: parsedData.results[i].geometry.location.lng
});
restaurant.set("placeId", placeId);
restaurant.set("name", name);
restaurant.set("vicinity", vicinity);
restaurant.set("location", point);
restaurant.save(null, {
success: function (location) {
console.log("Object ID: " + location.id);
},
error: function (location, error) {
console.log("Failed to create object, with error code: " + error.message);
}
});
}
}, function (httpResponse) {
// Error
response.error('request failed with response code ' + httpResponse)
});});
As you can see, this HTTP request should return a total of 14 places. Unfortunately, it will only return 9 places and it would also seem that which 9 are return can change. I am assuming there is a problem with the way my function is put together. Can anyone help me remedy this issue. I would like to return as many places as I want based on the radius of the HTTP request.
Thank You
The http request is done right, with a promise that's fulfilled when the request is complete. But your then() block tries to create several objects in a loop, not waiting for them all to finish, and failing to call response.success. Fix it like this...
// break it into understandable chunks, too, so, here's a function
// to build a Locations object from the http data
function locationFromResult(result) {
var Location = Parse.Object.extend("Locations");
var restaurant = new Location();
var placeId = result.place_id;
var name = result.name;
var vicinity = result.vicinity;
var point = new Parse.GeoPoint({
latitude: result.geometry.location.lat,
longitude: result.geometry.location.lng
});
restaurant.set("placeId", placeId);
restaurant.set("name", name);
restaurant.set("vicinity", vicinity);
restaurant.set("location", point);
return restaurant;
}
Parse.Cloud.job("requestLocations", function (request, response) {
var url = 'https://maps.googleapis.com/maps/api/place/nearbysearch/json?location=29.7030428,-98.1364808&radius=900&types=restaurant&key=AIzaSyCTg0x68Q6lrCAo6-A37zkxge81jDEKpvo';
Parse.Cloud.httpRequest({url: url}).then(function (httpResponse) {
var parsedData = JSON.parse(httpResponse.text);
var locations = parsedData.results.map(function(result) {
return locationFromResult(result);
});
// this is important, saveAll of the new objects before returning
// this can also be accomplished by saving the objects individually and using Parse.Promise.when()
return Parse.Object.saveAll(locations);
}).then(function(result) {
response.success(JSON.stringify(result));
}, function(error) {
response.error(JSON.stringify(error));
});
});
Related
I am trying to use promises with loops and nested functions that are within some of those loops. I have a series of functions that are supposed to bring back SharePoint list items from REST calls - once those are done executing, then another function gets called that uses the data that was brought back.
Since there are multiple lists, and subsequently multiple list items in each, I used a while loop to make each REST call, and from there, the data (list items) are put into objects. Those objects gets placed into an array and that's what that second functions uses to continue on.
I'm having trouble receiving the responses from the promises. I was thinking to have multiple promises that get pushed to an array, then finally use Promise.all to see if everything resolved before using then. The problem is that all the promises stay pending since I'm not returning the resolve correctly. Please see below.
function onQuerySuccess(sender, args) {
var itemEnumerator = items.getEnumerator();
while (itemEnumerator.moveNext()) {
var promise = new Promise(function (resolve, reject) {
var item = itemEnumerator.get_current();
item = item.get_item('URL');
var itemUrl = item.get_description();
getRequestItemsFromList(itemUrl);
});
promises.push(promise); // all promises are present, but their status is pending
}
console.log(promises);
Promise.all(promises).then(function (val) {
console.log(val);
execFuncs(); // function to execute once all the above are done
}).catch(function (response) {
console.log(response);
});
}
Because there's a lot of functions involved, so this is the order of execution:
getRequestItemsFromList //gets url for each list
execCrossDomainRequest (on success call) // makes REST call to get list and its items
cleanData // trims data and puts it in objects
The last is where I figured I call Promise.resolve() since that's the end of the line.
Either way, that's not working. I checked out other threads, but I'm trying to do this without using any libraries. Thanks in advance.
Edit:
Full relevant code:
var promises = [];
window.requests = [];
function getRequestLists() {
var requestsLists = hostWeb.get_lists().getByTitle('Name'); // sharepoint list with all the request list urls.
context.load(requestsLists);
var camlQuery = new SP.CamlQuery();
camlQuery.set_viewXml('<View></View>');
var items = requestsLists.getItems(camlQuery);
context.load(items, 'Include(URL)');
context.executeQueryAsync(onQuerySuccess, onQueryFail);
function onQuerySuccess(sender, args) {
var itemEnumerator = items.getEnumerator();
while (itemEnumerator.moveNext()) {
var promise = new Promise(function (resolve, reject) {
var item = itemEnumerator.get_current();
item = item.get_item('URL');
var itemUrl = item.get_description();
getRequestItemsFromList(itemUrl);
});
promises.push(promise);
}
console.log(promises);
Promise.all(promises).then(function (val) {
console.log(val);
execFuncs(); // not shown here
}).catch(function (response) {
console.log(response);
});
}
function onQueryFail(sender, args) {
alert("Request to retrieve list URL items has failed. " + args.get_message());
}
}
function getRequestItemsFromList(url) {
var lastPos = getSubstringIndex(url, "/", 4);
var webUrl = url.substring(0, lastPos); // truncates list url to parse out web url
var absListPos = getSubstringIndex(url, "AllItems.aspx", 1);
var absListUrl = url.substring(0, absListPos); // truncates the AllItems.aspx at the end of the list url
var relListPos = getSubstringIndex(absListUrl, "/", 3);
var relListUrl = absListUrl.substring(relListPos, absListUrl.length); // gets the list's relative url
var listName = "List Name";
console.log(webUrl);
execCrossDomainRequest(webUrl, listName, absListUrl);
}
function execCrossDomainRequest(webUrl, listName, absListUrl) {
var executor = new SP.RequestExecutor(appWebUrl);
executor.executeAsync({ // to collect the list description
url: appWebUrl + "/_api/SP.AppContextSite(#target)/web/lists/getbytitle(#name)?" +
"#target='" + webUrl + "'&#name='" + listName + "'" +
"&$select=Description",
method: "GET",
headers: { "Accept": "application/json; odata=verbose" },
success: onCallSuccess,
error: onCallFail
});
function onCallSuccess(data) {
var json = JSON.parse(data.body);
var description = json.d.Description;
executor.executeAsync({ // to collect the list item information
url: appWebUrl + "/_api/SP.AppContextSite(#target)/web/lists/getbytitle(#name)/items?" +
"#target='" + webUrl + "'&#name='" + listName + "'" +
"&$top=500&$select=*," +
"Assigned_x0020_To/Title" +
"&$expand=Assigned_x0020_To/Id",
method: "GET",
headers: { "Accept": "application/json; odata=verbose" },
success: onItemsCallSuccess,
error: onItemsCallFail
});
function onItemsCallSuccess(data) {
var itemsJson = JSON.parse(data.body);
var results = itemsJson.d.results;
cleanData(results, description, absListUrl);
}
function onItemsCallFail(data, errorCode, errorMessage) {
console.log("Could not make list items cross domain call. " + errorMessage);
}
}
function onCallFail(data, errorCode, errorMessage) {
console.log("Could not make list cross domain call. " + errorMessage);
}
}
function cleanData(results, listDescription, absListUrl) {
if (!results.length) {
return;
}
for (var i = 0; i < results.length; i++) {
var client = listDescription;
var id = results[i].ID;
...
}
var request = new Request(client, id, path, title, status, estimated, assignedTo, priority, description, response);
window.requests.push(request);
}
return Promise.resolve();
}
When you use a promise constructor like this:
var promise = new Promise(function (resolve, reject) {
It implies that somewhere inside this block you are calling resolve and/or reject, to settle the promise.
But you are never doing it, leaving the created promise object in pending state forever.
See Constructing a promise.
It seems to me your promises array is not defined: do var promises = []; before your while loop.
In the app I'm creating a user will be able to send a description of an object to a number of recipients (from 1 to 200). Using Parse Cloud Code I'll have to use a promise to wait for the email server response for every email (Mailgun).
Is there any other way where I stock all those created emails in some kind of array and send them in 1 shot to the email server? Otherwise I hit the max of 15 seconds a function can run.
Right now I use this:
Parse.Cloud.define("emailobject", function(request, response) {
// ... some company info parameters
var theTraders = request.params.traders;
var objectQ = new Parse.Query("objects");
objectQ.equalTo("objectId", theObjectId);
objectQ.first({
success:function(theObject) {
// ... more code
searchObjectPictures(theObject, {
success:function(pictureObjects) {
for (var a = 0; a < theTraders.length; a++) {
// create the parameters to create the email
var mailingParameters = {};
// ... create the parameters
// when the email html has been compiled
mailgun.sendWelcomeEmail({
to: traderEmail,
from: toString,
subject: subjectString,
html: mailing.getMailingHTML(mailingParameters)
}, {
success:function(httpResponse) {
console.log(httpResponse);
},
error:function(httpResponse) {
console.log(httpResponse);
}
});
}
// emailedObjectsToSave is an array of analytics objects
Parse.Object.saveAll(emailedObjectsToSave, {
success:function(list) {
response.success(true);
},
error:function(error) {
response.error(error);
}
});
},
error:function(error) {
response.error(error);
}
});
},
error:function(error){
response.error(error);
}
});
});
I know promises would be better for nested queries, but I'm still wrapping my head around this.
Thank you
After a lot of searching, trial and error, I have finally found the solution. As #danh said it's better to add these requests to a job queue. The objects are now saved to a "notifications" class that has a bool "notified" to check if this object has already been sent or not.
I'll post the code here for people who might also search for it.
First you add your objects to the new "notifications" class (queue).
Parse.Cloud.define("addToNotificationsQueue", function(request, response) {
var roleName = request.params.role;
var objects = request.params.objects;
var newEmailObjectsToSave = [];
var EmailedObjectClass = Parse.Object.extend("notifications");
var emailObjectACL = new Parse.ACL();
emailObjectACL.setPublicReadAccess(false);
emailObjectACL.setRoleReadAccess(roleName, true);
emailObjectACL.setRoleWriteAccess(roleName, true);
for (var i = 0; i < objects.length; i++) {
// define the parameters for the new objects based on the request params
var emailObject = new EmailedObjectClass();
// set all the details to your new emailObject
emailObject.setACL(emailObjectACL);
newEmailObjectsToSave.push(emailObject);
}
Parse.Object.saveAll(newEmailObjectsToSave, {
success:function(list) {
response.success(true);
},
error:function(error) {
response.error(error);
}
});
});
Then define your job. I run it every 15 minutes for now to test. When the mailserver starts acting faster, I'll set the limit and frequency higher.
Parse.Cloud.job("sendNotifications", function(request, status) {
Parse.Cloud.useMasterKey();
var numberOfNotificationsHandled;
var query = new Parse.Query("notifications");
query.limit(10); // limit because max 15 minutes, slow email server...
query.notEqualTo("notified", true);
query.find().then(function(notifications) {
numberOfNotificationsHandled = notifications.length;
var promise = Parse.Promise.as();
_.each(notifications, function(notification) {
var parameterDict = {};
// add all parameters the emailObject function needs
promise = promise.then(function(){
return Parse.Cloud.run("emailObject", parameterDict).then(function(result) {
notification.set("notified", true);
notification.save();
}, function(error) {
notification.set("notified", false);
notification.save();
});
});
});
return promise;
}).then(function() {
console.log("JOB COMPLETED");
status.success("Sent " + numberOfNotificationsHandled + " emails");
}, function(error) {
console.log("JOB ERROR " + error);
status.error("Job error " + error);
});
});
The emailObject method is just an httpRequest like any other. This method waits for the answers of those httpRequests.
I know this question have been asked many times, but I can't make it work.
Here is my situation. I had a string called data, and I want to unshorten all the link inside that string.
Code:
var Bypasser = require('node-bypasser');
var URI = require('urijs');
var data = 'multiple urls : http://example.com/foo http://example.com/bar';
var result = URI.withinString(data, function(url) {
var unshortenedUrl = null;
var w = new Bypasser(url);
w.decrypt(function(err, res) {
// How can I return res ?
unshortenedUrl = res;
});
// I know the w.descrypt function is a asynchronous function
// so unshortenedUrl = null
return unshortenedUrl;
});
Let's me walk you through the code.
URI.withinString will match all the URLs in data, manipulate it and return the result.
You can view an example from URI.js docs
What I want to with these URLs is to unshorten all of them using node-passer.
This is from node-bypasser document:
var Bypasser = require('node-bypasser');
var w = new Bypasser('http://example.com/shortlink');
w.decrypt(function(err, result) {
console.log('Decrypted: ' + result);
});
This is the result that I want multiple urls : http://example.com/foo_processed http://example.com/bar_processed
I created a notebook at tonicdev.com
Solution
var getUrlRegEx = new RegExp(
"(^|[ \t\r\n])((ftp|http|https|gopher|mailto|news|nntp|telnet|wais|file|prospero|aim|webcal):(([A-Za-z0-9$_.+!*(),;/?:#&~=-])|%[A-Fa-f0-9]{2}){2,}(#([a-zA-Z0-9][a-zA-Z0-9$_.+!*(),;/?:#&~=%-]*))?([A-Za-z0-9$_+!*();/?:~-]))"
, "g"
);
var urls = data.match(getUrlRegEx);
async.forEachLimit(urls, 5, function (url, callback) {
let w = new Bypasser(url);
w.decrypt(function (err, res) {
if (err == null && res != undefined) {
data = data.replace(url, res);
callback();
}
});
}, function(err) {
res.send(data);
});
You don't really understand what callback is. The callback serves to allow asynchronous code to run without Javascript waiting for it. If you were less lazy and added some debug in your code:
console.log("Started parsing");
var result = URI.withinString(data, function(url) {
console.log("URL parsed (or whatever)");
var unshortenedUrl = null;
var w = new Bypasser(url);
w.decrypt(function(err, res) {
// How can I return res ?
unshortenedUrl = res;
});
// I know the w.descrypt function is a asynchronous function
// so unshortenedUrl = null
return unshortenedUrl;
});
console.log("Call to library over");
You would (most likely) see messages in this order:
Started parsing
Call to library over
URL parsed (or whatever)
The answer: Callback is not guaranteed to run before any code you execute after assigning it. You can't put data in your result variable because the data might not be fetched yet.
I am trying to write a Cloud Code function that will allow me to edit the data of another user as I cannot do that in the application it self. What the code does (I should say tries to do as I don't know JS) is fetch a User object and a Group (a class I created) object using two separate queries based on the two object IDs inputed. Here is my code
Parse.Cloud.define("addInvite", function(request, response) {
Parse.Cloud.useMasterKey();
var userID = request.params.user;
var groupID = request.params.group;
var user;
var group;
var userQuery = new Parse.Query(Parse.User);
userQuery.equalTo("objectId", userID);
return userQuery.first
({
success: function(userRetrieved)
{
user = userRetrieved;
},
error: function(error)
{
response.error(error.message);
}
});
var groupObject = Parse.Object.extend("Group");
var groupQuery = new Parse.Query(groupObject);
groupQuery.equalTo("objectId", groupID);
return groupQuery.first
({
success: function(groupRetrieved)
{
group = groupRetrieved;
},
error: function(error)
{
response.error(error.message);
}
});
var relations = user.relation("invited");
relations.add(group);
user.save();
response.success();
});
Every time I execute the method I get the error:
[Error]: success/error was not called (Code: 141, Version: 1.9.0)
Can anyone help with this? Thanks.
Every function in Parse Cloud returns a Promise. This also includes any query functions which you run to retrieve some data. Clearly in your code you are returning a Promise when you execute a query which abruptly ends your cloud function when your query completes. As you do not call a response.success() or response.error() in any of the success blocks, your cloud function returns without setting a suitable response, something that Parse requires and hence the error. Your code needs to chain all the promises to ensure your code is executed correctly and return success/error in the last step:
Parse.Cloud.define("addInvite", function(request, response) {
Parse.Cloud.useMasterKey();
var userID = request.params.user;
var groupID = request.params.group;
var user;
var group;
var userQuery = new Parse.Query(Parse.User);
userQuery.equalTo("objectId", userID);
userQuery.first().then(function(userRetrieved) {
user = userRetrieved;
var groupObject = Parse.Object.extend("Group");
var groupQuery = new Parse.Query(groupObject);
groupQuery.equalTo("objectId", groupID);
return groupQuery.first();
}).then( function(groupRetrieved) {
//group = groupRetrieved;
var relations = user.relation("invited");
relations.add(groupRetrieved);
return user.save();
}).then(function() {
response.success();
}, function(error) {
response.error(error.message);
});
});
What I am trying to do here are:
Remove all contents in a class first, because every day the events.json file will be updated. I have my first question here: is there a better way to remove all contents from a database class on Parse?
Then I will send a request to get the events.json and store "name" and "id" of the result into a 2D array.
Then I will send multiple requests to get json files of each "name" and "id" pairs.
Finally, I will store the event detail into database. (one event per row) But now my code will terminate before it downloaded the json files.
Code:
function newLst(results) {
var event = Parse.Object.extend("event");
for (var i = 0; i < results.length; i++){
Parse.Cloud.httpRequest({
url: 'https://api.example.com/events/'+ results[i].name +'/'+ results[i].id +'.json',
success: function(newLst) {
var newJson = JSON.parse(newLst.text);
var newEvent = new event();
newEvent.set("eventId",newJson.data.id);
newEvent.set("eventName",newJson.data.title);
newEvent.save(null, {
success: function(newEvent) {
alert('New object created with objectId: ' + newEvent.id);
},
error: function(newEvent, error) {
alert('Failed to create new object, with error code: ' + error.message);
}
});
},
error: function(newLst) {
}
});
}
};
Parse.Cloud.job("getevent", function(request, status) {
var event = Parse.Object.extend("event");
var query = new Parse.Query(event);
query.notEqualTo("objectId", "lol");
query.limit(1000);
query.find({
success: function(results) {
for (var i = 0; i < results.length; i++) {
var myObject = results[i];
myObject.destroy({
success: function(myObject) {
},
error: function(myObject, error) {
}
});
}
},
error: function(error) {
alert("Error: " + error.code + " " + error.message);
}
});
var params = { url: 'https://api.example.com/events.json'};
Parse.Cloud.httpRequest(params).then(function(httpResponse) {
var results = [];
var jsonobj = JSON.parse(httpResponse.text);
for (var i = 0; i < jsonobj.data.length; i++) {
var tmp2D = {"name":"id"}
tmp2D.name = [jsonobj.data[i].name];
tmp2D.id = [jsonobj.data[i].id];
results.push(tmp2D);
}
newLst(results);
}).then(function() {
status.success("run job");
}, function(error) {
status.error(error);
});
});
I think my original answer is correct as a standalone. Rather than make it unreadable with the additional code, here it is made very specific to your edit.
The key is to eliminate passed callback functions. Everything below uses promises. Another key idea is decompose the activities into logical chunks.
A couple of caveats: (1) There's a lot of code there, and the chances that either your code is mistaken or mine is are still high, but this should communicate the gist of a better design. (2) We're doing enough work in these functions that we might bump into a parse-imposed timeout. Start out by testing all this with small counts.
Start with your question about destroying all instances of class...
// return a promise to destroy all instances of the "event" class
function destroyEvents() {
// is your event class really named with lowercase? uppercase is conventional
var query = new Parse.Query("event");
query.notEqualTo("objectId", "lol"); // doing this because the OP code did it. not sure why
query.limit(1000);
return query.find().then(function(results) {
return Parse.Object.destroyAll(results);
});
}
Next, get remote events and format them as simple JSON. See the comment. I'm pretty sure your idea of a "2D array" was ill-advised, but I may be misunderstanding your data...
// return a promise to fetch remote events and format them as an array of objects
//
// note - this differs from the OP data. this will evaluate to:
// [ { "name":"someName0", id:"someId0" }, { "name":"someName1", id:"someId1" }, ...]
//
// original code was producing:
// [ { "name":["someName0"], id:["someId0"] }, { "name":["someName1"], id:["someId1"] }, ...]
//
function fetchRemoteEvents() {
var params = { url: 'https://api.example.com/events.json'};
return Parse.Cloud.httpRequest(params).then(function(httpResponse) {
var results = [];
var remoteEvents = JSON.parse(httpResponse.text).data;
for (var i = 0; i < remoteEvents.length; i++) {
var remoteEvent = { "name": remoteEvents[i].name, "id": remoteEvents[i].id };
results.push(remoteEvent);
}
return results;
});
}
Please double check all of my work above regarding the format (e.g. response.text, JSON.parse().data, etc).
Its too easy to get confused when you mix callbacks and promises, and even worse when you're generating promises in a loop. Here again, we break out a simple operation, to create a single parse.com object based on one of the single remote events we got in the function above...
// return a promise to create a new native event based on a remoteEvent
function nativeEventFromRemoteEvent(remoteEvent) {
var url = 'https://api.example.com/events/'+ remoteEvent.name +'/'+ remoteEvent.id +'.json';
return Parse.Cloud.httpRequest({ url:url }).then(function(response) {
var eventDetail = JSON.parse(response.text).data;
var Event = Parse.Object.extend("event");
var event = new Event();
event.set("eventId", eventDetail.id);
event.set("eventName", eventDetail.title);
return event.save();
});
}
Finally, we can bring it together in a job that is simple to read, certain to do things in the desired order, and certain to call success() when (and only when) it finishes successfully...
// the parse job removes all events, fetches remote data that describe events
// then builds events from those descriptions
Parse.Cloud.job("getevent", function(request, status) {
destroyEvents().then(function() {
return fetchRemoteEvents();
}).then(function(remoteEvents) {
var newEventPromises = [];
for (var i = 0; i < remoteEvents.length; i++) {
var remoteEvent = remoteEvents[i];
newEventPromises.push(nativeEventFromRemoteEvent(remoteEvent));
}
return Parse.Promise.when(newEventPromises);
}).then(function() {
status.success("run job");
}, function(error) {
status.error(error);
});
});
The posted code does just one http request so there's no need for an array of promises or the invocation of Promise.when(). The rest of what might be happening is obscured by mixing the callback parameters to httpRequest with the promises and the assignment inside the push.
Here's a clarified rewrite:
Parse.Cloud.job("getevent", function(request, status) {
var promises = [];
var params = { url: 'https://api.example.com'};
Parse.Cloud.httpRequest(params).then(function(httpResponse) {
var results = [];
var jsonobj = JSON.parse(httpResponse.text);
for (var i = 0; i < jsonobj.data.length; i++) {
// some code
}
}).then(function() {
status.success("run job");
}, function(error) {
status.error(error);
});
});
But there's a very strong caveat here: this works only if ("// some code") that appears in your original post doesn't itself try to do any asynch work, database or otherwise.
Lets say you do need to do asynch work in that loop. Move that work to a promise-returning function collect those in an array, and then use Promise.when(). e.g....
// return a promise to look up some object, change it and save it...
function findChangeSave(someJSON) {
var query = new Parse.Query("SomeClass");
query.equalTo("someAttribute", someJSON.lookupAttribute);
return query.first().then(function(object) {
object.set("someOtherAttribute", someJSON.otherAttribute);
return object.save();
});
}
Then, in your loop...
var jsonobj = JSON.parse(httpResponse.text);
var promises = [];
for (var i = 0; i < jsonobj.data.length; i++) {
// some code, which is really:
var someJSON = jsonobj.data[i];
promises.push(findChangeSave(someJSON));
}
return Parse.Promise.when(promises);