I was solving a problem on a platform, but not getting how to do it?Can anyone help me out for this?
Problem Description
You are given the following API -
GET /api/comments
This will return a list of all comments. A comment object contains the following information
userId - ID of the user who commented
data - comment data
Given a userId, return an Array of comment data of all the comments by the given user.
Note
Apart from .json(), don’t use any other methods on the response object returned from fetch() call. This can cause your tests to fail.
Input - userId - the user id whose comment is to be returned.
Output - A list of comments by the given user id
Sample input 1 -
userId = 1
Sample API response
comments = [
{
'userId': '1',
"data": 'This looks slick!'
},
{
'userId': '2',
"data": 'I think this can be improved.'
},
{
'userId': '1',
"data": 'What kind of improvement?'
}]
Sample output 1 - ['This looks slick!', 'What kind of improvement?']
Below code that we have to complete
// TODO - Implement getCommentsByUserId() function
async function getCommentsByUserId(userId)
{
let v=await fetch(`/api/comments/${userId}`,{})
.then(response=>)
}
// ----------- Don't modify -----------
const mockFetch = (url, responseData) => {
const mockJsonPromise = Promise.resolve(responseData);
const mockFetchPromise = (callUrl) => {
if (url === callUrl) {
return Promise.resolve({
json: () => mockJsonPromise
});
} else {
return Promise.reject('404: No such url')
}
}
global.fetch = mockFetchPromise;
}
const successResponse = [
{
'userId': '1',
"data": 'This looks slick!'
},
{
'userId': '2',
"data": 'I think this can be improved.'
},
{
'userId': '1',
"data": 'What kind of improvement?'
}];
mockFetch('/api/comments', successResponse);
module.exports = getCommentsByUserId;
// ----------- Don't modify -----------
getCommentsByUserId("1").then((res) => {
console.log(res);
});
Use Array.prototype.filter:
let v = await fetch(`/api/comments/${userId}`,{})
.then(response=> response.json())
.then(comments => comments.filter(comment => comment.userId === userId))
You are lacking some knowledge on how async/await works. For filtering, as Alan has said, just use Array.prototype.filter.
When calling fetch, it returns a Promise. If you await fetch(...), the code will wait till fetch is finished, and returns the response of that API call.
async functions always returns a Promise (unless called with await). However, when writing the return statement in async functions, you must return a normal value.
async function getCommentsByUserId(userId)
{
let response = await fetch(`/api/comments/${userId}`,{});
let comments = response.json();
return comments.filter(comment => comment.userId === userId);
}
Either that, or using a normal function.
function getCommentsByUserId(userId)
{
return fetch(`/api/comments/${userId}`,{})
.then(res => res.json())
.then(comments => comments.filter(comment => comment.userId === userId));
}
Your test codes has many problems as well:
mockFetch('/api/comments', successResponse);: Your test code is mocking calling for the url "/api/comments/1", while you are mocking the API call for "/api/comments"
json: () => mockJsonPromise: The Response.json() functions is expected to return an actual value, and not a promise.
Here is my fixed version.
const mockFetch = (url, responseData) => {
return (callUrl) => {
if (url === callUrl) {
return Promise.resolve({
json: () => (responseData)
});
} else {
return Promise.reject('404: No such url')
}
};
}
const successResponse = [
{
'userId': '1',
"data": 'This looks slick!'
},
{
'userId': '2',
"data": 'I think this can be improved.'
},
{
'userId': '1',
"data": 'What kind of improvement?'
}];
global.fetch = mockFetch('/api/comments/1', successResponse);
async function getCommentsByUserId(userId) {
let response = await fetch(`/api/comments/${userId}`, {});
let comments = response.json();
return comments.filter(comment => comment.userId === userId);
}
getCommentsByUserId("1").then((res) => {
console.log(res);
});
I have made a few changes, now it's working fine but still, it doesn't clear any test cases.
However, it returns the same output as required.
// TODO - Implement getCommentsByUserId() function
async function getCommentsByUserId(userId) {
try{
let response = await fetch(`/api/comments`, {});
let comments = response.json();
comments = comments.filter(comment => comment.userId === userId);
a=[]
for(i of comments){
a.push(i.data)
}
return a;
}
catch{
return('404: No such url')
}
}
// ----------- Don't modify -----------
const mockFetch = (url, responseData) => {
const mockJsonPromise = Promise.resolve(responseData);
const mockFetchPromise = (callUrl) => {
if (url === callUrl) {
return Promise.resolve({
json: () => (responseData)
});
} else {
return Promise.reject('404: No such url')
}
}
global.fetch = mockFetchPromise;
}
const successResponse = [
{
'userId': '1',
"data": 'This looks slick!'
},
{
'userId': '2',
"data": 'I think this can be improved.'
},
{
'userId': '1',
"data": 'What kind of improvement?'
}];
mockFetch('/api/comments', successResponse);
module.exports = getCommentsByUserId;
// ----------- Don't modify -----------
getCommentsByUserId("1").then((res) => {
console.log(res);
});
Related
I use the Firestore Rest API in nextJs getServersideProps to fetch a firestore doc. It works as expected, but every 5:30min the function getServersideProps gets retriggered without reloading or navigating (is this only on dev environment?) and then the result of the rest api is simply
[ { readTime: '2022-10-28T14:24:01.348248Z' } ]
The document key is missing and no data is present, which breaks the server function (App behaves without showing error).
The fetching function looks like this:
const { GoogleToken } = require('gtoken');
const { documentToJson } = require('./helpers');
const getConfig = require('next/config').default;
const FIRESTORE = getConfig().serverRuntimeConfig.firestore;
export async function fetchWebsitePropsByPath(path: string) {
const body = JSON.stringify({
structuredQuery: {
from: [{ collectionId: 'websites' }],
where: {
compositeFilter: {
op: 'AND',
filters: [
{
fieldFilter: {
field: {
fieldPath: 'path',
},
op: 'ARRAY_CONTAINS',
value: {
stringValue: path,
},
},
},
],
},
},
limit: 1,
},
});
// Authenticate with Google
const gtoken = new GoogleToken({
key: FIRESTORE.key,
email: FIRESTORE.email,
scope: ['https://www.googleapis.com/auth/datastore'], // or space-delimited string of scopes
eagerRefreshThresholdMillis: 5 * 60 * 1000,
});
const getToken = () =>
new Promise((resolve, reject) => {
gtoken.getToken((err, token) => {
if (err) {
reject(err);
}
resolve(token);
});
});
const token: any = await getToken();
let headers = new Headers();
headers.append('Authorization', 'Bearer ' + token.access_token);
const res = await fetch(`${FIRESTORE.api}:runQuery`, {
method: 'POST',
headers,
body: body,
});
const rawData = await res.json();
const id = rawData[0].document.name.split('/').pop();
const docData = documentToJson(rawData[0].document.fields);
docData.id = id;
return docData;
}
I would like to know if I can prevent the refetching every 5:30 min if it is not dev env specific and why the rest api returns nothing here.
I am trying to create a function for my custom resolver that gets all documents in a collection and returns an amended payload with new data. Below is the code that im using to get one client and amend its data:
exports = (input) => {
const clientId = input._id;
const openStatusId = new BSON.ObjectId("898999");
const mongodb = context.services.get("mongodb-atlas");
const clientRecords = mongodb.db("db-name").collection("clients");
const jobRecords = mongodb.db("db-name").collection("jobs");
let client = clientRecords.findOne({"_id": clientId});
const query = { "client_id": clientId};
let jobsForClient = jobRecords.count(query)
.then(items => {
console.log(`Successfully found ${items} documents.`)
// items.forEach(console.log)
return items
})
.catch(err => console.error(`Failed to find documents: ${err}`));
let openJobs = jobRecords.count({"client_id": clientId,"status": openStatusId})
.then(numOfDocs => {
console.log(`Found ${numOfDocs} open jobs.`)
// items.forEach(console.log)
return numOfDocs
})
.catch(err => console.error(`Failed to find documents: ${err}`));
return Promise.all([client, jobsForClient, openJobs]).then(values => {
return {...values[0], "jobs": values[1], "openJobs": values[2]}
})
};
How can i fix this function to get all clients and loop over them to add data to each client?
I understand that changing this:
let client = clientRecords.findOne({"_id": clientId});
to this
let clients = clientRecords.find();
will get all the documents from the clients collection. How would i loop over each client after that?
UPDATE:
I have updated the function to the below and it works when running it in the realm environment but gives me an error when running it as a GraphQL query.
Updated code:
exports = (input) => {
const openStatusId = new BSON.ObjectId("999999");
const mongodb = context.services.get("mongodb-atlas");
const clientRecords = mongodb.db("db-name").collection("clients");
const jobRecords = mongodb.db("db-name").collection("jobs");
const clients = clientRecords.find();
const formatted = clients.toArray().then(cs => {
return cs.map((c,i) => {
const clientId = c._id;
const query = { "client_id": clientId};
let jobsForClient = jobRecords.count(query)
.then(items => {
console.log(`Successfully found ${items} documents.`)
// items.forEach(console.log)
return items
})
.catch(err => console.error(`Failed to find documents: ${err}`));
let openJobs = jobRecords.count({"client_id": clientId,"status": openStatusId})
.then(numOfDocs => {
console.log(`Found ${numOfDocs} open jobs.`)
// items.forEach(console.log)
return numOfDocs
})
.catch(err => console.error(`Failed to find documents: ${err}`));
return Promise.all([jobsForClient, openJobs]).then(values => {
return {...c, "jobs": values[0], "openJobs": values[1]}
});
})
}).catch(err => console.error(`Failed: ${err}`));
return Promise.all([clients, formatted]).then(values => {
return values[1]
}).catch(err => console.error(`Failed to find documents: ${err}`));
};
Error in GraphQL:
"message": "pending promise returned that will never resolve/reject",
It looks like you need wait for the last promise in your function to resolve before the function returns. I would do something like this:
exports = async (input) => {
...
let values = await Promise.all([jobsForClient, openJobs]);
return {...c, "jobs": values[0], "openJobs": values[1]};
}
Managed to solve by using mongodb aggregate. Solution below:
exports = async function(input) {
const openStatusId = new BSON.ObjectId("xxxxxx");
const mongodb = context.services.get("mongodb-atlas");
const clientRecords = mongodb.db("xxxxx").collection("xxxx");
const jobRecords = mongodb.db("xxxxx").collection("xxxx");
return clientRecords.aggregate([
{
$lookup: {
from: "jobs",
localField: "_id",
foreignField: "client_id",
as: "totalJobs"
}
},
{
$addFields: {
jobs: { $size: "$totalJobs" },
openJobs: {
$size: {
$filter: {
input: "$totalJobs",
as: "job",
cond: { "$eq": ["$$job.status", openStatusId]},
}
}
},
}
}
]);
};
Please help me figure out the difference in return behaviour between the onCall and onRequest google functions below.
onCall, the problem: returns null on all returns, except at the first return (as commented below). The db entries and rest of the code works fine. Just no returns problem.
onRequest, returns perfectly fine on every return. The db entries and rest of the code also works fine.
Both as you will see compare the same, but I just can't seem to get it to work at all. Any advice on how to get my returns to work for the onCall (and structure it better) would be much appreciated.
I am keen on sticking with async await (as opposed to a promise). Using Node.js 12. I am calling the onCall in Flutter, don't know if that is relevant for the question.
The onCall:
exports.applyUserDiscount = functions.https.onCall(async (data, context) => {
if (!context.auth) return {message: "Authentication Required!", code: 401};
const uid = context.auth.uid;
const discountCode = data["discountCode"];
const cartTotal = data["cartTotal"];
try {
return await db.collection("discountCodes").where("itemID", "==", discountCode).limit(1).get()
.then(async (snapshot) => {
if (snapshot.empty) {
return "doesNotExist"; // The only return that works.
} else { // Everything else from here onwards returns null.
snapshot.forEach(async (doc) => {
if (doc.data().redeemed == true) {
return "codeUsed";
} else {
const newCartTotal = cartTotal - doc.data().discountAmount;
if (newCartTotal < 0) {
return "lessThanTotal";
} else {
doc.ref.update({
redeemed: true,
uid: uid,
redeemDate: fireDateTimeNow,
});
await db.collection("userdata").doc(uid).set({
cartDiscount: admin.firestore.FieldValue.increment(-doc.data().discountAmount),
}, {merge: true});
return doc.data().discountAmount.toString();
}
}
});
}
});
} catch (error) {
console.log("Error:" + error);
return "error";
}
});
The onRequest:
exports.applyUserDiscount = functions.https.onRequest(async (req, res) => {
const uid = req.body.uid;
const discountCode = req.body.discountCode;
const cartTotal = req.body.cartTotal;
try {
return await db.collection("discountCodes").where("itemID", "==", discountCode).limit(1).get()
.then(async (snapshot) => {
if (snapshot.isempty) {
res.send("doesNotExist");
} else {
snapshot.forEach(async (doc) => {
if (doc.data().redeemed == true) {
res.send("codeUsed");
} else {
const newCartTotal = cartTotal - doc.data().discountAmount;
if (newCartTotal < 0) {
res.send("lessThanTotal");
} else {
doc.ref.update({
redeemed: true,
uid: uid,
redeemDate: fireDateTimeNow,
});
await db.collection("userdata").doc(uid).set({
cartDiscount: admin.firestore.FieldValue.increment(-doc.data().discountAmount),
}, {merge: true});
res.send(doc.data().discountAmount.toString());
}
}
});
}
});
} catch (error) {
console.log(error);
res.send("error");
}
});
There are several points to be noted when looking at your code(s):
You should not use async/await within a forEach loop. The problem is that the callback passed to forEach() is not being awaited, see more explanations here or here. HOWEVER, in your case you don't need to loop over the QuerySnapshot since it contains only one doc. You can use the docs property which return an array of all the documents in the QuerySnapshot and take the first (and unique) element.
You mix-up then() with async/await, which is not recommended.
I would advise to throw exceptions for the "error" cases, like doesNotExist, codeUsed or lessThanTotal but it's up to you to choose. The fact that, for example, the lessThanTotal case is an error or a standard business case is debatable... So if you prefer to send a "text" response, I would advise to encapsulate this response in a Object with one property: in your front-end the response will always have the same format.
So, the following should do the trick. Note that I send back on object with a response element, including for the cases that could be considered as errors. As said above you could throw an exception in these cases.
exports.applyUserDiscount = functions.https.onCall(async (data, context) => {
if (!context.auth) ... //See https://firebase.google.com/docs/functions/callable#handle_errors
const uid = context.auth.uid;
const discountCode = data["discountCode"];
const cartTotal = data["cartTotal"];
try {
const snapshot = await db.collection("discountCodes").where("itemID", "==", discountCode).limit(1).get();
if (snapshot.empty) {
//See https://firebase.google.com/docs/functions/callable#handle_errors
} else {
const uniqueDoc = snapshot.docs[0];
if (uniqueDoc.data().redeemed == true) {
return { response: "codeUsed" };
} else {
const newCartTotal = cartTotal - uniqueDoc.data().discountAmount;
if (newCartTotal < 0) {
return { response: "lessThanTotal" };
} else {
await uniqueDoc.ref.update({ // See await here!!
redeemed: true,
uid: uid,
redeemDate: fireDateTimeNow,
});
await db.collection("userdata").doc(uid).set({
cartDiscount: admin.firestore.FieldValue.increment(-uniqueDoc.data().discountAmount),
}, { merge: true });
return {
response: uniqueDoc.data().discountAmount.toString()
}
}
}
}
} catch (error) {
console.log("Error:" + error);
return "error";
}
});
I really want to know what concept(s) of javascript (regarding async/await) I am missing here. I am pretty sure this is not a duplicate question.
My code is too involved to show as an example so I will attempt to describe it the best I can and show the issue in its simplest form.
The main purpose of this code is to make a set of network requests in parallel and do some actions after they all are completed. I have that working properly, the loop execution 'appears' to pause until the awaited value is returned and this is desired.
However when I use a local .json file (loaded via require) rather than using axios.get, the loop runs all the way through before the awaited value is returned. This is problematic since I am mutatating the awaited value based on the premise of the loop pausing.
/*
Simplified as much as possible.
Note: The code works as desired when globalOptions.useNetworkStub = false
*/
const axios = require('axios').default;
const globalOptions = {
useNetworkStub: true
}
const getSearchByTerm = async(term) => {
if (globalOptions.useNetworkStub) {
const networkStub = require('./fake-response.json')
return Promise.resolve(networkStub)
}
return axios.get('https://jsonplaceholder.typicode.com/todos/1')
}
const getSearchesByTerms = async(terms = ['cats','dogs']) => {
const results = []
let result
try {
for (let i = 0; i < terms.length; i++) {
const term = terms[i]
result = await getSearchByTerm(term)
result.data && (result.data.searchTerm = term) // The issue is here!
results.push(result)
}
} catch (err) {
return Promise.reject(`getSearchesByTerms() Failed: ${err}`)
}
// ... code truncated here to keep things simple ...
return Promise.resolve(results)
}
getSearchesByTerms()
.then((responses) => {
let merged = {responses: []}
for (const response of responses) {
merged.responses.push(response.data)
}
console.log(`SUCCESS, data: ${JSON.stringify(merged, null, 2)}`)
})
.catch(e=>console.log(e))
fake-response.json
{
"data": {
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
}
}
As I metioned earlier, when axios is used the end result is correct. The first response has a key value pair of searchTerm: 'cats' and the second response has a key value pair of searchTerm: 'dogs'
When the local .json file is used both the first and second reponses have the same key value pair of searchTerm: 'dogs'. This is the problem.
EDIT: changed const term = terms[i].term to const term = terms[i]
ANOTHER EDIT: fixed typos in code, added data for fake-response.json and posted a working example of this issue here on repl.it
Minus a few typos, your code runs -- with some issues.
// example.js
const axios = require('axios').default;
const globalOptions = {
useNetworkStub: true
}
const getSearchByTerm = async function(term) {
if (globalOptions.useNetworkStub) {
const networkStub = require('./fake-response.json')
return Promise.resolve(networkStub)
}
return axios.get('https://some-live-endpoint.com/', {params:{q: term}})
}
const getSearchesByTerms = async function(terms = ['cats','dogs']) {
const results = []
let result
try {
for (let i = 0; i < terms.length; i++) {
const term = terms[i]
result = await getSearchByTerm(term)
console.log("Term: ", term, "Result: ", result); // added this log to clarify your issue
result.data && (result.data.searchTerm = term) // The issue is here!
results.push(result)
}
} catch (err) {
return Promise.reject(`getSearchesByTerms() Failed: ${err}`)
}
// ... code truncated here to keep things simple ...
return Promise.resolve(results)
}
getSearchesByTerms()
.then((responses) => {
let merged = {responses: []}
for (const response of responses) {
merged.responses.push(response.data)
}
console.log(`SUCCESS, data: ${JSON.stringify(merged, null, 2)}`)
})
.catch(e=>console.log(e))
And here's and example json file
{
"data": {
"payload": "Success"
}
}
Here's the output you'll get:
Term: cats Result: { data: { payload: 'Success' } }
Term: dogs Result: { data: { payload: 'Success', searchTerm: 'cats' } }
SUCCESS, data: {
"responses": [
{
"payload": "Success",
"searchTerm": "dogs"
},
{
"payload": "Success",
"searchTerm": "dogs"
}
]
}
The thing to notice is that your issue isn't async it's using a reference to the same object for both results. This is a lesson that can burn you in a lot of subtle but important ways in Javascript -- and many other languages that hide the complexity of pointers from the programmer. You should generally avoid mutating objects.
Here's a version that uses JS spread syntax to copy the object instead of mutating.
const axios = require('axios').default;
const globalOptions = {
useNetworkStub: true
}
const getSearchByTerm = async function(term) {
if (globalOptions.useNetworkStub) {
const networkStub = require('./fake-response.json')
return Promise.resolve(networkStub)
}
return axios.get('https://some-live-endpoint.com/', {params:{q: term}})
}
const getSearchesByTerms = async function(terms = ['cats','dogs']) {
const results = []
let result
try {
for (let i = 0; i < terms.length; i++) {
const term = terms[i]
result = await getSearchByTerm(term)
console.log("Term: ", term, "Result: ", result); // added this log to clarify your issue
if (result && "data" in result) {
results.push({ data: { ...result.data, term }}) // copies instead of mutating original object
}
}
} catch (err) {
return Promise.reject(`getSearchesByTerms() Failed: ${err}`)
}
// ... code truncated here to keep things simple ...
return Promise.resolve(results)
}
getSearchesByTerms()
.then((responses) => {
let merged = {responses: []}
for (const response of responses) {
merged.responses.push(response.data)
}
console.log(`SUCCESS, data: ${JSON.stringify(merged, null, 2)}`)
})
.catch(e=>console.log(e))
Here's a version that mutates in the way you were hoping it would work. The important change being that the stub has more than one objects that you can query:
// newExample.js
// gets a new object each time, so mutation doesn't break
const axios = require('axios').default;
const globalOptions = {
useNetworkStub: true
}
const getSearchByTerm = async function(term) {
if (globalOptions.useNetworkStub) {
const networkStub = require('./fake-response.json')[term]
return Promise.resolve(networkStub)
}
return axios.get('https://some-live-endpoint.com/', {params:{q: term}})
}
const getSearchesByTerms = async function(terms = ['cats','dogs']) {
const results = []
let result
try {
for (let i = 0; i < terms.length; i++) {
const term = terms[i]
result = await getSearchByTerm(term)
console.log("Term: ", term, "Result: ", result); // added this log to clarify your issue
result.data && (result.data.searchTerm = term) // no longer mutating same object
results.push(result)
}
} catch (err) {
return Promise.reject(`getSearchesByTerms() Failed: ${err}`)
}
// ... code truncated here to keep things simple ...
return Promise.resolve(results)
}
getSearchesByTerms()
.then((responses) => {
let merged = {responses: []}
for (const response of responses) {
merged.responses.push(response.data)
}
console.log(`SUCCESS, data: ${JSON.stringify(merged, null, 2)}`)
})
.catch(e=>console.log(e))
// fake-response.json
{
"cats": {
"data": {
"payload": "Success for cats!"
}
},
"dogs": {
"data": {
"payload": "Success for dogs!"
}
}
}
Still. If you're worried about deep cloning -- I recommend you plan your output in a way where you don't have to mutate or clone the value:
// better.js
// plans output to not mutate or copy
const axios = require('axios').default;
const globalOptions = {
useNetworkStub: true
}
const getSearchByTerm = async function(term) {
if (globalOptions.useNetworkStub) {
const networkStub = require('./fake-response.json')[term]
return Promise.resolve(networkStub)
}
return axios.get('https://some-live-endpoint.com/', {params:{q: term}})
}
const getSearchesByTerms = async function(terms = ['cats','dogs']) {
const results = []
let result
try {
for (let i = 0; i < terms.length; i++) {
const term = terms[i]
result = await getSearchByTerm(term)
console.log("Term: ", term, "Result: ", result); // added this log to clarify your issue
if (result && "data" in result) {
results.push({ term, data: result.data }) // doesn't copy or mutate result
}
}
} catch (err) {
return Promise.reject(`getSearchesByTerms() Failed: ${err}`)
}
// ... code truncated here to keep things simple ...
return Promise.resolve(results)
}
getSearchesByTerms()
.then((responses) => {
let merged = {responses: []}
for (const response of responses) {
merged.responses.push(response) // grabbing while response
}
console.log(`SUCCESS, data: ${JSON.stringify(merged, null, 2)}`)
})
.catch(e=>console.log(e))
I'm using an HTTP-triggered Firebase cloud function to make an HTTP request. I get back an array of results (events from Meetup.com), and I push each result to the Firebase realtime database. But for each result, I also need to make another HTTP request for one additional piece of information (the category of the group hosting the event) to fold into the data I'm pushing to the database for that event. Those nested requests cause the cloud function to crash with an error that I can't make sense of.
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
const request = require('request');
exports.foo = functions.https.onRequest(
(req, res) => {
var ref = admin.database().ref("/foo");
var options = {
url: "https://api.meetup.com/2/open_events?sign=true&photo-host=public&lat=39.747988&lon=-104.994945&page=20&key=****",
json: true
};
return request(
options,
(error, response, body) => {
if (error) {
console.log(JSON.stringify(error));
return res.status(500).end();
}
if ("results" in body) {
for (var i = 0; i < body.results.length; i++) {
var result = body.results[i];
if ("name" in result &&
"description" in result &&
"group" in result &&
"urlname" in result.group
) {
var groupOptions = {
url: "https://api.meetup.com/" + result.group.urlname + "?sign=true&photo-host=public&key=****",
json: true
};
var categoryResult = request(
groupOptions,
(groupError, groupResponse, groupBody) => {
if (groupError) {
console.log(JSON.stringify(error));
return null;
}
if ("category" in groupBody &&
"name" in groupBody.category
) {
return groupBody.category.name;
}
return null;
}
);
if (categoryResult) {
var event = {
name: result.name,
description: result.description,
category: categoryResult
};
ref.push(event);
}
}
}
return res.status(200).send("processed events");
} else {
return res.status(500).end();
}
}
);
}
);
The function crashes, log says:
Error: Reference.push failed: first argument contains a function in property 'foo.category.domain._events.error' with contents = function (err) {
if (functionExecutionFinished) {
logDebug('Ignoring exception from a finished function');
} else {
functionExecutionFinished = true;
logAndSendError(err, res);
}
}
at validateFirebaseData (/user_code/node_modules/firebase-admin/node_modules/#firebase/database/dist/index.node.cjs.js:1436:15)
at /user_code/node_modules/firebase-admin/node_modules/#firebase/database/dist/index.node.cjs.js:1479:13
at Object.forEach (/user_code/node_modules/firebase-admin/node_modules/#firebase/util/dist/index.node.cjs.js:837:13)
at validateFirebaseData (/user_code/node_modules/firebase-admin/node_modules/#firebase/database/dist/index.node.cjs.js:1462:14)
at /user_code/node_modules/firebase-admin/node_modules/#firebase/database/dist/index.node.cjs.js:1479:13
at Object.forEach (/user_code/node_modules/firebase-admin/node_modules/#firebase/util/dist/index.node.cjs.js:837:13)
at validateFirebaseData (/user_code/node_modules/firebase-admin/node_modules/#firebase/database/dist/index.node.cjs.js:1462:14)
at /user_code/node_modules/firebase-admin/node_modules/#firebase/database/dist/index.node.cjs.js:1479:13
at Object.forEach (/user_code/node_modules/firebase-admin/node_modules/#firebase/util/dist/index.node.cjs.js:837:13)
at validateFirebaseData (/user_code/node_modules/firebase-admin/node_modules/#firebase/database/dist/index.node.cjs.js:1462:14)
If I leave out the bit for getting the group category, the rest of the code works fine (just writing the name and description for each event to the database, no nested requests). So what's the right way to do this?
I suspect this issue is due to the callbacks. When you use firebase functions, the exported function should wait on everything to execute or return a promise that resolves once everything completes executing. In this case, the exported function will return before the rest of the execution completes.
Here's a start of something more promise based -
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
const request = require("request-promise-native");
exports.foo = functions.https.onRequest(async (req, res) => {
const ref = admin.database().ref("/foo");
try {
const reqEventOptions = {
url:
"https://api.meetup.com/2/open_events?sign=true&photo-host=public&lat=39.747988&lon=-104.994945&page=20&key=xxxxxx",
json: true
};
const bodyEventRequest = await request(reqEventOptions);
if (!bodyEventRequest.results) {
return res.status(200).end();
}
await Promise.all(
bodyEventRequest.results.map(async result => {
if (
result.name &&
result.description &&
result.group &&
result.group.urlname
) {
const event = {
name: result.name,
description: result.description
};
// get group information
const groupOptions = {
url:
"https://api.meetup.com/" +
result.group.urlname +
"?sign=true&photo-host=public&key=xxxxxx",
json: true
};
const categoryResultResponse = await request(groupOptions);
if (
categoryResultResponse.category &&
categoryResultResponse.category.name
) {
event.category = categoryResultResponse.category.name;
}
// save to the databse
return ref.push(event);
}
})
);
return res.status(200).send("processed events");
} catch (error) {
console.error(error.message);
}
});
A quick overview of the changes -
Use await and async calls to wait for things to complete vs. being triggered in a callback (async and await are generally much easier to read than promises with .then functions as the execution order is the order of the code)
Used request-promise-native which supports promises / await (i.e. the await means wait until the promise returns so we need something that returns a promise)
Used const and let vs. var for variables; this improves the scope of variables
Instead of doing checks like if(is good) { do good things } use a if(isbad) { return some error} do good thin. This makes the code easier to read and prevents lots of nested ifs where you don't know where they end
Use a Promise.all() so retrieving the categories for each event is done in parallel
There are two main changes you should implement in your code:
Since request does not return a promise you need to use an interface wrapper for request, like request-promise in order to correctly chain the different asynchronous events (See Doug's comment to your question)
Since you will then call several times (in parallel) the different endpoints with request-promise you need to use Promise.all() in order to wait all the promises resolve before sending back the response. This is also the case for the different calls to the Firebase push() method.
Therefore, modifying your code along the following lines should work.
I let you modifying it in such a way you get the values of name and description used to construct the event object. The order of the items in the results array is exactly the same than the one of the promises one. So you should be able, knowing that, to get the values of name and description within results.forEach(groupBody => {}) e.g. by saving these values in a global array.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
var rp = require('request-promise');
exports.foo = functions.https.onRequest((req, res) => {
var ref = admin.database().ref('/foo');
var options = {
url:
'https://api.meetup.com/2/open_events?sign=true&photo-host=public&lat=39.747988&lon=-104.994945&page=20&key=****',
json: true
};
rp(options)
.then(body => {
if ('results' in body) {
const promises = [];
for (var i = 0; i < body.results.length; i++) {
var result = body.results[i];
if (
'name' in result &&
'description' in result &&
'group' in result &&
'urlname' in result.group
) {
var groupOptions = {
url:
'https://api.meetup.com/' +
result.group.urlname +
'?sign=true&photo-host=public&key=****',
json: true
};
promises.push(rp(groupOptions));
}
}
return Promise.all(promises);
} else {
throw new Error('err xxxx');
}
})
.then(results => {
const promises = [];
results.forEach(groupBody => {
if ('category' in groupBody && 'name' in groupBody.category) {
var event = {
name: '....',
description: '...',
category: groupBody.category.name
};
promises.push(ref.push(event));
} else {
throw new Error('err xxxx');
}
});
return Promise.all(promises);
})
.then(() => {
res.send('processed events');
})
.catch(error => {
res.status(500).send(error);
});
});
I made some changes and got it working with Node 8. I added this to my package.json:
"engines": {
"node": "8"
}
And this is what the code looks like now, based on R. Wright's answer and some Firebase cloud function sample code.
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
const request = require("request-promise-native");
exports.foo = functions.https.onRequest(
async (req, res) => {
var ref = admin.database().ref("/foo");
var options = {
url: "https://api.meetup.com/2/open_events?sign=true&photo-host=public&lat=39.747988&lon=-104.994945&page=20&key=****",
json: true
};
await request(
options,
async (error, response, body) => {
if (error) {
console.error(JSON.stringify(error));
res.status(500).end();
} else if ("results" in body) {
for (var i = 0; i < body.results.length; i++) {
var result = body.results[i];
if ("name" in result &&
"description" in result &&
"group" in result &&
"urlname" in result.group
) {
var groupOptions = {
url: "https://api.meetup.com/" + result.group.urlname + "?sign=true&photo-host=public&key=****",
json: true
};
var groupBody = await request(groupOptions);
if ("category" in groupBody && "name" in groupBody.category) {
var event = {
name: result.name,
description: result.description,
category: groupBody.category.name
};
await ref.push(event);
}
}
}
res.status(200).send("processed events");
}
}
);
}
);