I'm building a mobile app and on some actions (like userCreate), I trigger some Firebase functions to perform an API call to a third service, and then write something in the Firestore database.
Everything works in theory, but in practice, the API calls are quite fast (even with cold start scenarios), but the database writes can take several MINUTES to complete (if they do at all, I suspect that sometimes it takes too long and times out).
Since the API call work just fine, and so does the DB write on some occasion, I suspect that this is simply due to very poor async management from me since I know about nothing in JS.
Here are two of many example functions which are concerned by this issue, just to showcase that it happens whereas I'm triggering functions onCreate or on HTTPS.
onCreate function
const functions = require("firebase-functions");
const axios = require('axios')
// The Firebase Admin SDK to access the Firestore.
const admin = require('firebase-admin');
admin.initializeApp();
// Third party service credentials generation during onCreate request
exports.
buildCredentials = functions.auth.user().onCreate((user) => {
// Request to Third party to generate an Client Access Token
axios({
method: "post",
url: "https://api.ThirdParty.com/api/v1/oauth/token",
data: "client_id=xxx&client_secret=yyy&grant_type=client_credentials&scope=all:all",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
})
.then(function (response) {
//handle success
console.log('new user created: ')
console.log(user.id);
console.log(response.data);
// We write a new document in the users collection with the user ID and the Client Access Token
const db = admin.firestore();
const newUser = {
uid: user.uid,
clientAccessToken: response.data.access_token
};
db.collection('users').doc(user.uid).set(newUser)
.catch(function (error) {
//handle error
console.log(error);
});
})
.catch(function (response) {
//handle error
console.log(response);
});
})
HTTPS onCall function
exports.paymentRequest = functions.https.onCall(async (data, context) => {
const clientAccessToken = data.clientAccessToken;
const recipientIban = data.recipientIban;
const recipientName = data.recipientName;
const paymentDescription = data.paymentDescription;
const paymentReference = data.paymentReference;
const productPrice = parseInt(data.productPrice);
const uid = data.uid;
const jsonData = {
"destinations": [
{
"accountNumber": recipientIban,
"type": "iban"
}
],
"amount": productPrice,
"currency": "EUR",
"market": "FR",
"recipientName": recipientName,
"sourceMessage": paymentDescription,
"remittanceInformation": {
"type": "UNSTRUCTURED",
"value": paymentReference
},
"paymentScheme": "SEPA_INSTANT_CREDIT_TRANSFER"
};
(async function(){
response = await axios({
method: "post",
url: "https://api.ThirdParty.com/api/v1/pay",
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: `Bearer ${clientAccessToken}`,},
data: jsonData,
})
// We write a the payment request ID in the user's document
const db = admin.firestore();
const paymentRequestID = response.data.id;
db.collection('users').doc(uid).set({
paymentRequestID: paymentRequestID
}, { merge: true })
.catch(function (error) {
//handle error
console.log(error);
});
console.log(response.data)
return response.data
})()
})
Am I on the right track thinking that this is an async problem?
Or is it a Firebase/Firestore issue?
Thanks
You are not returning the promises returned by the asynchronous methods (axios() and set()), potentially generating some "erratic" behavior of the Cloud Function.
As you will see in the three videos about "JavaScript Promises" from the official Firebase video series you MUST return a Promise or a value in a background triggered Cloud Function, to indicate to the platform that it has completed, and to avoid it is terminated before the asynchronous operations are done or it continues running after the work has been completed.
The following adaptations should do the trick (untested):
onCreate Function:
buildCredentials = functions.auth.user().onCreate((user) => {
// Request to Third party to generate an Client Access Token
return axios({
method: "post",
url: "https://api.ThirdParty.com/api/v1/oauth/token",
data: "client_id=xxx&client_secret=yyy&grant_type=client_credentials&scope=all:all",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
})
.then(function (response) {
//handle success
console.log('new user created: ')
console.log(user.id);
console.log(response.data);
// We write a new document in the users collection with the user ID and the Client Access Token
const db = admin.firestore();
const newUser = {
uid: user.uid,
clientAccessToken: response.data.access_token
};
return db.collection('users').doc(user.uid).set(newUser)
})
.catch(function (response) {
//handle error
console.log(response);
return null;
});
})
Callable Function:
exports.paymentRequest = functions.https.onCall(async (data, context) => {
try {
const clientAccessToken = data.clientAccessToken;
const recipientIban = data.recipientIban;
const recipientName = data.recipientName;
const paymentDescription = data.paymentDescription;
const paymentReference = data.paymentReference;
const productPrice = parseInt(data.productPrice);
const uid = data.uid;
const jsonData = {
// ...
};
const response = await axios({
method: "post",
url: "https://api.ThirdParty.com/api/v1/pay",
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: `Bearer ${clientAccessToken}`,
},
data: jsonData,
})
// We write a the payment request ID in the user's document
const db = admin.firestore();
const paymentRequestID = response.data.id;
await db.collection('users').doc(uid).set({
paymentRequestID: paymentRequestID
}, { merge: true })
console.log(response.data)
return response.data
} catch (error) {
// See https://firebase.google.com/docs/functions/callable#handle_errors
}
})
Related
I have twillio texting me everyday at 8am, a message from aws lambda but I want it to text me the weather for the day. Im trying to use a weather api but I cant figure out how to do it
I tried JSON.stringify() but it didnt work
const axios = require("axios");
const options = {
method: 'GET',
url: 'https://dark-sky.p.rapidapi.com/30.4383,-84.2807',
params: {units: 'auto', lang: 'en'},
headers: {
'X-RapidAPI-Key': 'XXXXXXXX',
'X-RapidAPI-Host': 'dark-sky.p.rapidapi.com'
}
};
axios.request(options).then(function (response) {
console.log(response.data);
}).catch(function (error) {
console.error(error);
});
exports.handler = (event, context, callback) => {
const accountSid = process.env.ACCOUNT_SID;
const authToken = process.env.AUTH_TOKEN;
const client = require('twilio')(accountSid, authToken);
// Send a text message
client.messages.create({
body: 'TEXT MESSAGE EXAMPLEi',
to: 'XXXXX', // your phone number
from: 'XXXXXXX' // a valid Twilio number
})
.then((message) => {
// Success, return message SID
callback(null, message.sid);
})
.catch((e) => {
// Error, return error object
callback(Error(e));
});
};
I am attempting to send data alongside the code for paypal to the node.js client, but I am not sure where to send that data alongside it.
Here is the client side paypal code:
paypal.Buttons({
// Order is created on the server and the order id is returned
createOrder: (data, actions) => {
return fetch("/api/orders", {
method: "post",
// use the "body" param to optionally pass additional order information
// like product ids or amount
})
.then((response) => response.json())
.then((order) => order.id);
},
// Finalize the transaction on the server after payer approval
onApprove: (data, actions) => {
return fetch(`/api/orders/${data.orderID}/capture`, {
method: "post",
})
.then((response) => response.json())
.then((orderData) => {
// Successful capture! For dev/demo purposes:
console.log('Capture result', orderData, JSON.stringify(orderData, null, 2));
const transaction = orderData.purchase_units[0].payments.captures[0];
alert(`Transaction ${transaction.status}: ${transaction.id}\n\nSee console for all available details`);
// When ready to go live, remove the alert and show a success message within this page. For example:
// const element = document.getElementById('paypal-button-container');
// element.innerHTML = '<h3>Thank you for your payment!</h3>';
// Or go to another URL: actions.redirect('thank_you.html');
});
}
}).render('#paypal-button-container');
Here is the server side paypal code:
app.post("/api/orders", async (req, res) => {
const order = await createOrder();
res.json(order);
});
// capture payment & store order information or fullfill order
app.post("/api/orders/:orderID/capture", async (req, res) => {
const { orderID } = req.params;
const captureData = await capturePayment(orderID);
// TODO: store payment information such as the transaction ID
res.json(captureData);
});
//////////////////////
// PayPal API helpers
//////////////////////
// use the orders api to create an order
async function createOrder() {
const accessToken = await generateAccessToken();
const url = `${base}/v2/checkout/orders`;
const response = await fetch(url, {
method: "post",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
intent: "CAPTURE",
purchase_units: [
{
amount: {
currency_code: "USD",
value: "100.00",
},
},
],
}),
});
const data = await response.json();
return data;
}
// use the orders api to capture payment for an order
async function capturePayment(orderId) {
const accessToken = await generateAccessToken();
const url = `${base}/v2/checkout/orders/${orderId}/capture`;
const response = await fetch(url, {
method: "post",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
});
const data = await response.json();
return data;
}
The issue is there is no req.body.stuff anywhere.
The goal I am trying to achieve, is is when the payment is made, I want to send the data of like the string "potato", Before the actual payment goes through on node. Because potato is a client side variable that will then when used in server retrieve the value of the amount to be paid in node.
Where/how do I put this so I can do this?
Thanks.
How to do it is literally explained in the comments of that client-side code:
return fetch("/api/orders", {
method: "post",
// use the "body" param to optionally pass additional order information
// like product ids or amount
})
I am trying to import a function from an external file I have, which contains a promise.
The resolution of the promise is what should be returned, but I get an error message:
The requested module './functions.js' does not provide an export named 'getOkapiToken'
The POST request works fine when I run it directly in the server, but I need it in a separate file as it will be used by several different files in the future.
This is also the first time I'm using promises, so I'm not sure I handled it properly (though I can't get any errors registered until I deal with the first one).
The functions.js file is built as follows:
import post from 'axios';
export function getOkapiToken(url, user, password) {
//Get username and password for API
const auth = {
"username": user,
"password": password
};
//Create headers for POST request
const options = {
method: 'post',
data: auth,
headers: {
'Content-Type': 'application/json',
'Content-Length': auth.length,
'X-Okapi-Tenant': 'diku'
}
};
//Make API post call
post(url+':9130/authn/login', options)
.then(response => {
return(response.headers['x-okapi-token'])
}).catch((err) => {
return(`There was an error 2: ${err}`)
})
}
And I try to import it as follows:
import { getOkapiToken } from './functions3.js'
import settings from './jsons/config.json';
let OkapiKey = await new Promise((resolve,reject) => {
//Call function to make API post call
let keytoken = getOkapiToken(settings.url,settings.userauth,settings.passauth)
console.log(`There was an error: ${keytoken}`)
if (keytoken.length == 201) {
resolve(keytoken)
} else {
reject('There was an error')
}
})
OkapiKey.then((data) => {
console.log(data)
})
.catch((err) => {
console.error('I have an error:'+err.code);
})
There are three ways to handle asynchronous task in Javascript and wit Node.JS
Pass in a Callback to run in the asynchronous code
Use a Promise that will either resolve or reject the promise
Use the async keyword in front of the function and place await in front of the asynchronous code.
With that said I was able to get the code to work by running a simple node server and I modified your code just a bit to get a response back from the server.
index.js
const { getOkapiToken } = require('./functions.js')
const settings = require('./settings.json')
var OkapiKey = getOkapiToken(settings.url,settings.userauth,settings.passauth)
OkapiKey
.then((data) => {
console.log('I have data'+ data.toString())
})
.catch((err) => {
console.error('I have an error:'+err.code);
})
functions.js
const post = require('axios');
const getOkapiToken = (url, user, password) =>
new Promise(function (resolve, reject) {
//Get username and password for API
const auth = {
"username": user,
"password": password
};
//Create headers for POST request
const options = {
method: 'post',
data: auth,
headers: {
'Content-Type': 'application/json',
'Content-Length': auth.length,
'X-Okapi-Tenant': 'diku'
}
};
post('http://localhost:3000/', options)
.then(response => {
resolve(response.data)
// if (response.headers['x-okapi-token']) {
// resolve(response.headers['x-okapi-token']);
// } else {
// reject((error));
// }
}).catch((err) => {
console.error('Response Error:'+err)
})
})
exports.getOkapiToken = getOkapiToken;
My NodeJS controller is sending the response before finish running the wait methods.
The response with the 'OK' message is immediately send back to the client (browser) when I run it. It sends the response before running all the methods.
try {
const ep = await new keysModel().getKeys(); // key is a list of objects
await ep.map( async (key) => {
// some operations with the keys here
// call a fetch to a third party API
await fetch(urlRequest, {
method: 'post',
body: JSON.stringify(myJSONObject),
headers: {
'Authorization': `Basic ${base64.encode(`${key.APIKey}:${key.PasswordKey}`)}`,
'Content-Type': 'application/json'
},
})
.then(async res => res.json())
.then(async json => {
// Here I have some operations with the json response data
// It includes some database queries
})
.catch(async err => {
// await method to send me an email
res.status(501).send('error');
});
});
res.status(202).send('OK');
} catch() {
// await method to send me an email
res.status(501).send('error');
}
Below is the route:
const express = require('express');
const router = express.Router();
router.get('/myPath', myController.testMethod);
I'm new with NodeJS. Trying to understand how it works.
Thanks
I think Promise.all should work, eg. use async await with array map :)
try {
const ep = await new keysModel().getKeys(); // key is a list of objects
await Promise.all(ep.map(async (key) => {
// some operations with the keys here
// call a fetch to a third party API
return fetch(urlRequest, {
method: 'post',
body: JSON.stringify(myJSONObject),
headers: {
'Authorization': `Basic ${base64.encode(`${key.APIKey}:${key.PasswordKey}`)}`,
'Content-Type': 'application/json'
},
})
.then(async res => res.json())
.then(async json => {
// Here I have some operations with the json response data
// It includes some database queries
})
.catch(async err => {
// await method to send me an email
res.status(501).send('error');
});
}));
res.status(202).send('OK');
} catch() {
// await method to send me an email
res.status(501).send('error');
}
By default, map is not async function, using async on map's call back would not be asynced.
you can use one of Promise.all, Promise.allSettled, Promise.any, Promise.race (depends on your needs, please read more about Promises here.
short example for your case, i will use Promise.all for the example.
try {
const ep = await new keysModel().getKeys(); // key is a list of objects
Promise.all(
ep.map((key) => {
// some operations with the keys here
// call a fetch to a third party API
fetch(urlRequest, {
method: 'post',
body: JSON.stringify(myJSONObject),
headers: {
'Authorization': `Basic
${base64.encode(`${key.APIKey}:${key.PasswordKey}`)}`,
'Content-Type': 'application/json'
},
})
.then(/* Handle array of responses */)
.catch(/* Handle errors */);
);
// proceed..
} catch() {
// await method to send me an email
res.status(501).send('error');
}
I created a small server using NodeJS/Express and I'm using node-fetch to interact with SurveyMonkeys API. I currently have two surveys on my account which I can view through their Postman collection. But when I try to use my own endpoints, it doesn't seem to work. The GET request to view all of the surveys returns a status code of "200" but responds with:
{
"size": 0,
"timeout": 0
}
The POST request to create a survey gives me a status code of "400" but returns the same response. Here is my code so far.
const router = require("express").Router();
const fetch = require("node-fetch");
const TOKEN = process.env.SM_ACCESS_TOKEN;
const BASEURL = process.env.SM_BASEURL;
const options = method => ({
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
method: method
}
});
/*
GET a list of surveys
*/
router.get("/", async (req, res) => {
try {
const surveys = await fetch(`${BASEURL}surveys`, options("GET"));
console.log(surveys);
if (surveys) {
return res.status(200).json(surveys);
}
} catch (err) {
console.log(err);
res.status(500).send({ message: "Server error", err });
}
});
router.post("/create-survey", (req, res) => {
const surveyData = req.body;
fetch(`${BASEURL}surveys`, {
method: "POST",
body: surveyData,
headers: {
Authorization: `bearer ${TOKEN}`,
"Content-Type": "application/json"
}
})
.then(data => {
return res.status(data.status).json(data);
})
.catch(err => console.log(err));
});
module.exports = router;
Additional information:
I am able to complete all of these actions using the POSTMAN collection provided by SurveyMonkey with my Access Token. BASEURL = "https://api.surveymonkey.com/v3/".
ServeyData = { "title": "Some Title" }
Resolved this issue by switching out of node-fetch and instead using axios. Could be the fetch vs xhr request I think.