Chaining GET requests to WP REST API in Express - node.js

I am struggling to understand callbacks, promises, and async/await.
What I want to do is read a .csv file inside my project folder that contains 150+ post ID's.
For each one of those ID's I want to make a https GET request to fetch a JSON response from my Wordpress website.
Then for each one of those posts that gets returned I want to insert them in my Firestore database.
I'm struggling with how to properly set up the callback functions.
Please help.
Thanks in advance.
const express = require('express');
const router = express.Router();
const https = require("https");
const Recipe = require("../includes/newrecipe");
var admin = require('firebase-admin');
var serviceAccount = require("../service_key.json");
const collectionKey = "recipes"; //name of the collection
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "<MY_FIRESTORE_URL>"
});
const firestore = admin.firestore();
const fs = require('fs');
const parse = require('csv-parser');
function prepareCsvData() {
return new Promise((resolve, reject) => {
//establish empty csvData array and filename to be referenced
var csvData = [];
var filename = 'wprm_recipe_ids.csv';
//read the csv file and push the data object into the array
fs.createReadStream(filename)
.pipe(parse(['ID']))
.on('data', (data) => csvData.push(data))
.on('end', () => { resolve(csvData); });
});
}
function getRecipeFromBlog(recipeId) {
return new Promise((resolve, reject) => {
//make the get request to my website to get the recipe
https.get('<MY_WEBSITE_URL>' + recipeId, (response) => {
var body = "";
response.on('data', function (chunk) { body += chunk; });
response.on('end', () => {
var { recipe } = JSON.parse(body);
//build new recipe to be exported
var newRecipe = new Recipe(recipe);
resolve(newRecipe);
});
});
});
}
/* GET recipes. */
router.get('/', async (req, res, next) => {
//first prepare the csv data
//function returns a promise with the csv data
//that I can then use in the next step
const csvData = await prepareCsvData();
for (var i = 0; csvData.length < i; i++) {
getRecipeFromBlog(csvData[i].ID)
.then((newRecipe) => {
//when I have a recipe for a given recipe ID
//update database in firestore
firestore
.collection(collectionKey)
.doc(""+newRecipe.id)
.set(newRecipe)
.then(function() {
console.log('document written');
});
});
}
res.send('done');
});

You need to do something like below:
Play around this, You'll get it working hopefully!
Let me know if that worked!
router.get("/", async (req, res, next) => {
const csvData = await prepareCsvData();
const recipePromises = [];
// check if data is empty or not
if (!csvData.length) {
return res.send("Empty data");
}
csvData.forEach((element) => {
recipePromises.push(getRecipeFromBlog(element.id));
});
// await for all promises parallelly.
const result = await Promise.all(recipePromises);
// Get a new write batch
const batch = db.batch();
result.forEach((recipe) => {
const ref = db.collection("recipes").doc(`${recipe.id}`);
batch.set(ref, recipe);
});
// Commit the batch
await batch.commit();
res.send("done");
});

The OP code looks pretty close to working. Have the promise-returning functions been tested? Assuming they work, first decorate them as async...
async function prepareCsvData() {...}
async function getRecipeFromBlog(recipeId) {...}
Create another promise-returning function to insert many recipes into firebase...
async function writeRecipesToFB(recipes) {
const collectionRef = collection(collectionKey);
const promises = recipes.map(recipe => {
return collectionRef.doc(`${recipe.id}`).set(recipe);
});
return Promise.all(promises)
}
As another answer suggests, as an alternative, firebase's batch write is a good idea...
async function writeRecipesToFB(recipes) {
// as a set of promises
const collectionRef = collection(collectionKey);
const batch = db.batch();
recipes.forEach(recipe => {
const docRef = collectionRef.doc(`${recipe.id}`)
batch.set(docRef, recipe)
});
return batch.commit();
}
Now the express function is easy to write...
router.get('/', async (req, res, next) => {
const csvData = await prepareCsvData();
let promises = csvData.map(row => {
return getRecipeFromBlog(row.ID);
});
const recipes = await Promise.all(promises);
await writeRecipesToFB(recipes);
res.send('done');
});

Related

NodeJS - Returning 'undefined'

I am learning NodeJS (generally I write in PHP). Please help me figure out the Promises. Now the 'getDataFromDir' function returns 'undefined'. I read the documentation, but apparently I don't fully understand something. Thanks in advance!
const host = 'localhost';
const port = 3000;
const __dirname = process.cwd();
async function getDataFromDir(fileName){
fsp
.readdir(path.join(fileName))
.then(async (indir) => {
const list = []
for (const item of indir) {
const src = await fsp.stat(path.join(fileName, item))
list.push(item)
}
return list;
})
}
const server = http.createServer(async (req, res) => {
const result = await getDataFromDir(__dirname);
result
.then((list) => {
console.log(list);
});
});
It seems like your return statement is returning only for the callback function under your .then statement. You should be able to use the await statement with your initial request and achieve a similar result.
async function getDataFromDir(fileName){
let indir = await fsp.readdir(path.join(fileName));
const list = [];
for (const item of indir) {
const src = await fsp.stat(path.join(fileName, item));
list.push(item);
}
return list;
}

NodeJS await all url-exists before returning

I'm having an issue of my controller returning data before url-exists finishes running.
const urlExists = require('url-exists');
const ESAPI = require('node-esapi');
exports.getDocs = async (req, res, next) => {
try {
const id = req.params.tID;
let docs = [];
const getDocs = await models.Documents.getDocs(id); // Getting data from Database
for(const d of getDocs) {
const docName = ESAPI.encoder().encodeForHTML(d.DocumentName);
const path = `https://mywebsite/Files/${id}/${docName}`;
urlExists(path, function(err, exists) {
console.log(exists);
if(exists) {
docs.push({
path,
audited: d.Audited,
comment: d.Comment
});
}
});
}
console.log(docs);
return res.json(docs);
} catch(err) {
return res.json([]);
}
}
I can see in the console.logs() that it first logs the console.log(docs); an empty array. Then, it logs the console.log(exists). How can I wait until the for loop and urlExists finishes running before returning the docs array?
Thanks
urlExists is a callback-based function, you can promisify it and then await it.
To promisify urlExists function, you can use built-in node module: util.promisify.
// import the "util" module
const util = require('util');
// create a promise wrapper around "urlExists" function
const promisifiedUrlExists = util.promisify(urlExists);
After urlExists has been promisified, await it inside the loop.
exports.getDocs = async (req, res, next) => {
try {
...
for(const d of getDocs) {
...
const exists = await promisifiedUrlExists(path);
if(exists) {
docs.push({
path,
audited: d.Audited,
comment: d.Comment
});
}
}
return res.json(docs);
} catch(err) {
return res.json([]);
}
}

nodejs with supertest, calling an endpoint twice for different test cases getting a Did you forget to wait for something async in your test?

i have a single endpoint where i call twice on 2 different describes to test different responses
const express = require("express");
const router = express.Router();
const fs = require("fs");
const multer = require("multer");
const upload = multer({ dest: "files/" });
const csv = require("fast-csv");
let response = { message: "success" }
router.post("/post_file", upload.single("my_file"), (req, res) => {
let output = get_output(req.file.path);
fs.unlinkSync(req.file.path);
if(output.errors.length > 0) response.message = "errors found";
res.send(JSON.stringify(response))
})
const get_output = (path) => {
let errors = []
let fs_stream = fs.createReadStream(path);
let csv_stream = csv.parse().on("data", obj => {
if(!is_valid(obj)) errors.push(obj);
});
fs_stream.pipe(csv_stream);
return {errors};
}
const is_valid = (row) => {
console.log("validate row")
// i validate here and return a bool
}
my unit tests
const app = require("../server");
const supertest = require("supertest");
const req = supertest(app);
describe("parent describe", () => {
describe("first call", () => {
const file = "my path to file"
// this call succeeds
it("should succeed", async (done) => {
let res = await req
.post("/post_file")
.attach("my_file", file);
expect(JSON.parse(res.text).message).toBe("success")
done();
});
})
describe("second call", () => {
const file = "a different file"
// this is where the error starts
it("should succeed", async (done) => {
let res = await req
.post("/post_file")
.attach("my_file", file);
expect(JSON.parse(res.text).message).toBe("errors found")
done();
});
})
})
// csv file is this
NAME,ADDRESS,EMAIL
Steve Smith,35 Pollock St,ssmith#emailtest.com
I get the following
Cannot log after tests are done. Did you forget to wait for something async in your test?
Attempted to log "validate row".
The problem is that tested route is incorrectly implemented, it works asynchronously but doesn't wait for get_output to end and responds synchronously with wrong response. The test just reveals that console.log is asynchronously called after test end.
Consistent use of promises is reliable way to guarantee the correct execution order. A stream needs to be promisified to be chained:
router.post("/post_file", upload.single("my_file"), async (req, res, next) => {
try {
let output = await get_output(req.file.path);
...
res.send(JSON.stringify(response))
} catch (err) {
next(err)
}
})
const get_output = (path) => {
let errors = []
let fs_stream = fs.createReadStream(path);
return new Promise((resolve, reject) => {
let csv_stream = csv.parse()
.on("data", obj => {...})
.on("error", reject)
.on("end", () => resolve({errors}))
});
}
async and done shouldn't be mixed in tests because they serve the same goal, this may result in test timeout if done is unreachable:
it("should succeed", async () => {
let res = await req
.post("/post_file")
.attach("my_file", file);
expect(JSON.parse(res.text).message).toBe("success")
});

How to get Express Node route to wait for function before rendering

I am trying to get a route to wait for an async function in another module to return before render runs, but no matter what I do, res.render always runs first.
This is my current code, which actually just freezes and never loads:
router.get('/', function(req, res, next) {
try {
const cities = spreadsheet.getData()
} catch(err) {
console.log(err)
}
res.render('index', { cities: cities})
})
and the function it is waiting for is this:
exports.getData = function () {
parsedData = [];
accessSpreadsheet().then(function(data) {
console.log(parsedData)
return parsedData;
});
};
const accessSpreadsheet = async() => {
await doc.useServiceAccountAuth({
client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
private_key: process.env.GOOGLE_PRIVATE_KEY,
});
const loadedDoc = await doc.loadInfo();
sheet = await doc.sheetsByIndex[0];
const cells = await sheet.loadCells(allCells[cellsIndex]);
const data = await parseData();
const moreCells = await checkNextCells()
return;
}
The render runs first, and the parsedData prints in the console. I also tried making the route async, and I tried res.render inside a callback. Is there any way to make this work?
Since accessSpreadSheet is an async function, you either need to await or return the promise (as suggested by Patrick Roberts in the comment), in getData function, and similarly in the router.
Using async await you can update your code as below (not tested)
exports.getData = async function () {
parsedData = [];
parsedData = await accessSpreadSheet();
return parsedData;
};
const accessSpreadsheet = async() => {
await doc.useServiceAccountAuth({
client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
private_key: process.env.GOOGLE_PRIVATE_KEY,
});
const loadedDoc = await doc.loadInfo();
sheet = await doc.sheetsByIndex[0];
const cells = await sheet.loadCells(allCells[cellsIndex]);
const data = await parseData();
const moreCells = await checkNextCells()
return;
}
And in the router
router.get('/', async function(req, res, next) {
let cities;
try {
cities = await spreadsheet.getData()
} catch(err) {
console.log(err)
}
res.render('index', { cities: cities})
})
In your router, spreadsheet.getData() being async, you need to handle it with async/wait, which will require your callback to be async.
router.get('/', async function(req, res, next) {
try {
const cities = await spreadsheet.getData();
res.render('index', { cities: cities}) // moved this to inside a try block
} catch(err) {
console.log(err)
// you need to handle the exception here, return error message etc
}
})
In getData, you need to return a promise that will be resolved when called in router.
exports.getData = async function () {
let parsedData = [];
try {
parsedData = await accessSpreadsheet();
} catch (exc) {
// handle exception here
}
return parsedData;
};
finally in accessSpreadsheet(), I do not see where you return the data parsed.
const accessSpreadsheet = async() => {
try{
await doc.useServiceAccountAuth({
client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
private_key: process.env.GOOGLE_PRIVATE_KEY,
});
let loadedDoc = await doc.loadInfo();
let sheet = await doc.sheetsByIndex[0];
let cells = await sheet.loadCells(allCells[cellsIndex]); // cellsIndex is not defined
let data = await parseData(); // are you to pass the sheet? do not know how parsedData() works
let moreCells = await checkNextCells()
return data; // assuming data is what is meant to be returned
} catch(exc) {
// probably return null or re-raise exception
}
}
It is important to always use try/catch or then/catch when dealing with async code in NodeJS.
Hope it sheds some light!

How to wait for a url callback before send HTTP response in koa?

I have a koa router I need to call a api where will async return result. This means I cannot get my result immediately, the api will call my callback url when it's ok. But now I have to use it like a sync api which means I have to wait until the callback url is called.
My router like this:
router.post("/voice", async (ctx, next) => {
// call a API here
const params = {
data: "xxx",
callback_url: "http//myhost/ret_callback",
};
const req = new Request("http://xxx/api", {
method: "POST",
body: JSON.stringify(params),
});
const resp = await fetch(req);
const data = await resp.json();
// data here is not the result I want, this api just return a task id, this api will call my url back
const taskid = data.taskid;
// now I want to wait here until I got "ret_callback"
// .... wait .... wait
// "ret_callback" is called now
// get the answer in "ret_callback"
ctx.body = {
result: "ret_callback result here",
}
})
my callback url like this:
router.post("/ret_callback", async (ctx, next) => {
const params = ctx.request.body;
// taskid will tell me this answer to which question
const taskid = params.taskid;
// this is exactly what I want
const result = params.text;
ctx.body = {
code: 0,
message: "success",
};
})
So how can I make this aync api act like a sync api?
Just pass a resolve() to another function. For example, you can do it this way:
// use a map to save a lot of resolve()
const taskMap = new Map();
router.post("/voice", async (ctx, next) => {
// call a API here
const params = {
data: "xxx",
callback_url: "http//myhost/ret_callback",
};
const req = new Request("http://xxx/api", {
method: "POST",
body: JSON.stringify(params),
});
const resp = await fetch(req);
const data = await resp.json();
const result = await waitForCallback(data.taskid);
ctx.body = {
result,
} })
const waitForCallback = (taskId) => {
return new Promise((resolve, reject) => {
const task = {};
task.id = taskId;
task.onComplete = (data) => {
resolve(data);
};
task.onError = () => {
reject();
};
taskMap.set(task.id, task);
});
};
router.post("/ret_callback", async (ctx, next) => {
const params = ctx.request.body;
// taskid will tell me this answer to which question
const taskid = params.taskid;
// this is exactly what I want
const result = params.text;
// here you continue the waiting response
taskMap.get(taskid).onComplete(result);
// not forget to clean rubbish
taskMap.delete(taskid);
ctx.body = {
code: 0,
message: "success",
}; })
I didn't test it but I think it will work.
function getMovieTitles(substr) {
let movies = [];
let fdata = (page, search, totalPage) => {
let mpath = {
host: "jsonmock.hackerrank.com",
path: "/api/movies/search/?Title=" + search + "&page=" + page,
};
let raw = '';
https.get(mpath, (res) => {
res.on("data", (chunk) => {
raw += chunk;
});
res.on("end", () => {
tdata = JSON.parse(raw);
t = tdata;
totalPage(t);
});
});
}
fdata(1, substr, (t) => {
i = 1;
mdata = [];
for (i = 1; i <= parseInt(t.total_pages); i++) {
fdata(i, substr, (t) => {
t.data.forEach((v, index, arrs) => {
movies.push(v.Title);
if (index === arrs.length - 1) {
movies.sort();
if (parseInt(t.page) === parseInt(t.total_pages)) {
movies.forEach(v => {
console.log(v)
})
}
}
});
});
}
});
}
getMovieTitles("tom")
Okay so first of all, this should not be a "goal" for you. NodeJS works better as ASync.
However, let us assume that you still want it for some reason, so take a look at sync-request package on npm (there is a huge note on there that you should not this in production.
But, I hope you mean on how to make this API simpler (as in one call kinda thingy). You still need .next or await but it will be be one call anyway.
If that is the case, please comment on this answer I can write you a possible method I use myself.
How about this ?
router.post("/voice", async (ctx, next) => {
const params = {
data: "xxx",
callback_url: "http//myhost/ret_callback",
};
const req = new Request("http://xxx/api", {
method: "POST",
body: JSON.stringify(params),
});
const resp = await fetch(req);
const data = await resp.json();
// data here is not the result I want, this api just return a task id, this api will call my url back
const taskid = data.taskid;
let response = null;
try{
response = await new Promise((resolve,reject)=>{
//call your ret_callback and when it finish call resolve(with response) and if it fails, just reject(with error);
});
}catch(err){
//errors
}
// get the answer in "ret_callback"
ctx.body = {
result: "ret_callback result here",
}
});

Resources