Reduce array of functions that may be async and have callback - javascript

I'm curious how to create a simple version of express's middleware by creating an array of functions and using .reduce to loop over them.
function fullPath(context) {
context.fullPath = nodePath.isAbsolute(context.path) ? context.path : nodePath.join(context.cwd, context.path);
}
function extension(context) {
context.extension = nodePath.extname(context.path);
}
function contents(context, callback) {
return fs.readFile(context.fullPath, { encoding: 'utf8' }, (err, contents) => {
if (err) throw err;
context.contents = contents;
return callback(null, context);
});
}
function fileLoader(context, callback) {
return [
fullPath,
extension,
contents
].reduce((context, fn) => {
return fn(context, callback)
}, context)
}
What should the logic within .reduce be to cycle each function, pass a callback to each, and have them nest properly, then call the parent callback at the end?

Here's this working:
const nodePath = require('path');
const fs = require('fs');
function fullPath(context) {
console.log('fullPath')
context.fullPath = nodePath.isAbsolute(context.path) ? context.path : nodePath.join(context.cwd, context.path);
}
function extension(context) {
console.log('extension')
context.extension = nodePath.extname(context.path);
}
function contents(context, callback) {
console.log('contents')
return fs.readFile(context.fullPath, { encoding: 'utf8' }, (err, contents) => {
console.log('contents-i')
if (err) return callback(err);
context.contents = contents;
return callback();
});
}
function middleware(stack) {
return (context = {}, master) => {
var hasError = false
return stack.reduceRight((callback, fn) => {
if (hasError) return () => {};
return () => {
const isAsync = fn.length === 2
if (isAsync) {
fn(context, (err) => {
if (err) {
hasError = true
return master(err);
}
callback(null, context);
})
} else {
fn(context)
callback(null, context)
}
}
}, master)()
}
}
const fileLoader = middleware([fullPath, extension, contents])
fileLoader({ path: './example.md', cwd: '' }, (err, context) => {
console.log({ err, context })
})

Related

test is exiting before the method which has async.waterfall, is executed

I'm currently writing unit tests for an API. One of the helper functions is the async.waterfall to avoid "callback hell". I made this method async and await where the method is called.
The test is exiting saying Number of calls: 0 in Node.js. I'm using Jest as my test framework.
Method being tested:
outletInfoController.fetchAllOutletsByUserIdAndCityId = async function (req, res) {
var userId = req.body.userId;
var cityId = req.body.cityId;
await outletInfoHelper.fetchAllOutletsByUserIdAndCityId(userId, cityId, function (error, result) {
if (error || !result) {
responseUtils.buildAndRespond(commonConst.CODE.FAILURE_RESPONSE_CODE, commonConst.MESSAGE.FAILURE_RESPONSE_MESSAGE,
error, null, res);
} else {
responseUtils.buildAndRespond(commonConst.CODE.SUCCESS_RESPONSE_CODE, commonConst.MESSAGE.SUCCESS_RESPONSE_MESSAGE,
null, result, res);
}
});
};
Method which is using async.waterfall:
outletInfoHelper.fetchAllOutletsByUserIdAndCityId = async function (userId, cityId, outerCallback) {
async.waterfall(
[function getRestaurantsByUserId(callback) {
//do something
}, function getRestaurantsDetails(restIds, callback) {
//do something
}, function getRestaurantsUserCount(restaurants, callback) {
//do something
}], function (error, results) {
//do something
});
}
Test code snippet:
test("outletInfo_success_response", async () => {
let req = {
body: {
userId: 8881,
cityId: 211
}
};
let restIds = [];
await factory.restaurantListFactory((result) => {
restIds = result;
});
await fetchUserRestaurantObjFromUserId.mockImplementation((userId, cb) => {
cb(null, restIds);
});
let mockResponse = [];
await factory.restaurantObjectsFactory(function (result) {
mockResponse = result;
});
let successResp = {
statusMessage: 'success',
data: mockResponse,
errorMessage: null,
statusCode: 0
}
await fetchRestaurantsFromIdsAndCityId.mockImplementation((restIds, cityId, cb) => {
cb(null, mockResponse);
});
let response;
await factory.getUserRestMap((result) => {
response = result;
});
await fetchUsersFromOutletId.mockImplementation((restIds, cb) => {
cb(null, response);
});
let res = await getResponseObj();
await outletInfoController.fetchAllOutletsByUserIdAndCityId(req, res);
expect(res.send).toBeCalledWith(successResp);
});

undefined response on async action

I'm trying to launch an exe file from an Electron app with React/Redux.
From the component i'm doing dispatch(launch(titleId, titleName))
The problem is i'm getting path undefined when i'm waiting for readFolders() async.
Any idea what i'm doing wrong and what should i change in my aproach?
Thanks in advance!
launch.js
export const launch = async (titleId, titleName) => {
const path = await readFolders(titleId);
console.log('path:', path) //undefined
execFile(path, (err, data) => {
if (err) {
console.log('err', err);
} else if (data) {
console.log('data:', data)
} else {
console.log('success');
}
});
return {
type: 'LAUNCH',
};
};
readFolders.js
import fs from 'fs';
import { homedir } from 'os';
const fsPromises = fs.promises;
const isExeFile = file => file.match(/.*\.exe$/i);
export const readFolders = async titleId => {
const userDir = homedir();
const folderPath = `${userDir}/downloads`;
const fullPath = `${folderPath}/${titleId}`;
try {
const contents = await fsPromises.readdir(fullPath);
contents.forEach(async item => {
if (isExeFile(item)) {
console.log('isExeFile');
return `${fullPath}/${item}`;
}
try {
const nestedFolder = await fsPromises.readdir(`${fullPath}/${item}`);
nestedFolder.forEach(nestedItem => {
if (isExeFile(nestedItem)) {
return `${fullPath}/${item}/${nestedItem}`;
}
return null;
});
} catch (err) {
console.log('err:', err);
}
});
} catch (err) {
console.log('err main:', err);
}
};
Edit:
I also tried this way and now const path = await readFolders(titleId); returns the correct result, but this way eslint is complaining (https://eslint.org/docs/rules/no-async-promise-executor) and it doesn't feel like a good solution.
return new Promise(async (resolve, reject) => {
try {
const contents = await fsPromises.readdir(fullPath);
contents.forEach(async item => {
if (isExeFile(item)) {
console.log(`${fullPath}/${item}`);
return resolve(`${fullPath}/${item}`);
}
try {
const nestedFolder = await fsPromises.readdir(`${fullPath}/${item}`);
nestedFolder.forEach(nestedItem => {
if (isExeFile(nestedItem)) {
console.log(`${fullPath}/${item}/${nestedItem}`);
return resolve(`${fullPath}/${item}/${nestedItem}`);
}
return null;
});
} catch (err) {
console.log('err:', err);
reject(err);
}
});
} catch (err) {
console.log('err main:', err);
reject(err);
}
});
Missing return at the end. When you return in forEach, It returns from callback anonymous function only. return ${fullPath}/${item}/${nestedItem};
For more you can read my blog on it:
https://medium.com/#deepak_v/weird-part-how-to-break-the-loop-in-javascript-8bba3e658267
Updated code:
export const readFolders = async (titleId) => {
const userDir = homedir();
const folderPath = `${userDir}/downloads`;
const fullPath = `${folderPath}/${titleId}`;
try {
const contents = await fsPromises.readdir(fullPath);
let path = "";
contents.some(async (item) => {
if (isExeFile(item)) {
console.log("isExeFile");
path = `${fullPath}/${item}`;
return path;
}
try {
const nestedFolder = await fsPromises.readdir(`${fullPath}/${item}`);
const found = nestedFolder.some((nestedItem) => {
if (isExeFile(nestedItem)) {
path = `${fullPath}/${item}/${nestedItem}`;
return path;
}
return false;
});
if (found) return path;
else return false;
} catch (err) {}
});
return path;
} catch (err) {
console.log("err main:", err);
}
};

How to fetch the image files from a server and zip it in sailsjs

I want to zip all the images from s3 urls. I am doing it on server side on sailsjs framework.
I tried using axios to download the images and used 'zipdir'. The images are getting downloaded in temp folder. But its not getting zipped properly.
this.downloadFiles = function (req, res) {
var resObj = {}
async.waterfall([
this.createFolder.bind(undefined, req),
this.downloadFilesAxios.bind(undefined, req),
this.zipTheFiles.bind(undefined, req)
], function final(err, result) {
if (err) {
console.log('SOME ERROR', err);
resObj.statusCode = err.statusCode || 500;
} else {
resObj.statusCode = 200;
resObj.result = result.questionList;
}
console.log('------', resObj.statusCode)
resObj.messageKey = sails.config.statusCode[resObj.statusCode].key;
resObj.message = sails.config.statusCode[resObj.statusCode].message;
return res.send(resObj);
});
};
}
this.downloadFilesAxios = function (req, obj, callback) {
SurveyDocs.find({ surveyId: req.body.surveyId })
.exec(function (err, docsDetails) {
async.map(docsDetails, function (img, cb) {
const url = img.docS3Url;
let imageName = img.docFileName;
const path = Path.resolve(__dirname, "temp", imageName);
const writer = Fs.createWriteStream(path)
Axios({
method: 'get',
url: url,
responseType: 'stream'
})
.then(function (response) {
response.data.pipe(writer)
})
writer.on('finish', (done) => {
console.log('success!!!');
cb(null, null)
});
writer.on('error', (err) => {
console.log('failed!!!');
cb(err, null)
});
}, (err, data) => {
if (err) {
console.log('errrr', err);
}
callback(null, obj);
});
})
};
this.zipTheFiles = function (req, obj, callback) {
var surveyId = req.body.surveyId;
var tempDir = 'assets/zip/' + surveyId + '.zip'
zipdir('temp', { saveTo: tempDir }, function (err, buffer) {
callback(null, obj);
});
callback(null, obj);
}
Here I am getting a corrupt zip file. Please suggest the solution.
I tried out your example there are a few things you need to consider in order to make it work.
const async = require('async');
const fs = require('fs');
const path = require('path');
const zipDir = require('zip-dir');
const axios = require('axios');
let writer;
async.waterfall([
createFolder,
downLoadFileAxios,
zip
], function (err, result) {
if (err) {
console.log(err);
} else {
console.log('result :', result);
}
});
let's assume this method creates the temp folder
function createFolder(callback) {
setTimeout(function() {
callback(null, 'temp');
}, 1000);
}
Here the writeStream object and it's events should be put inside the then block. So that it writes the stream to the file correctly.
Another important thing here is you are not having a cath block attached the promise, so if any exception occurs it will be simply eaten up.
function downLoadFileAxios(dirPath, callback) {
// Hard coded the images url for the sake of simplicity
let files = [
'https://free-images.com/lg/be5e/climbing_helmets_climbing_equipment.jpg',
'https://free-images.com/lg/87ce/lilac_lilac_bush_lilac.jpg'
];
async.mapSeries(files, function(img, cb) {
let name = img.slice(img.lastIndexOf('/') + 1);
let imagePath = path.resolve(__dirname, "newDir", name);
writer = fs.createWriteStream(imagePath);
axios({
method: 'get',
url: img,
responseType: 'stream'
}).
then(function(response) {
response.data.pipe(writer);
writer.on('finish', (done) => {
console.log('success!!!');
cb(null, null)
});
writer.on('error', (err) => {
console.log('failed!!!');
cb(err, null)
});
})
.catch((err) => {
console.log(err);
})
}, function(err, result) {
if (err) {
console.log('errrr', err);
}
callback(null, 'done downloading');
})
}
function zip (dirPath, callback) {
let zipPath = path.resolve(__dirname, "assets", "file.zip");
// console.log(`got directory path : ${dirPath}`);
zipDir("newDir", {
saveTo: zipPath
}, function(err, buffer) {
if(err) {
callback(err, null);
} else {
callback(null, 'done');
}
});
}
This can be easily done using Async/Await like following.
const async = require('async');
const fs = require('fs');
const path = require('path');
const zipDir = require('zip-dir');
const axios = require('axios');
var writer;
// faking the directory creation part
async function createFolder(callback) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(true);
}, 2000);
});
}
//Executes in the specified order.
(async () => {
await createFolder();
await downLoadFile();
await zipTheFile();
})();
async function downLoadFile() {
let files = [
'https://free-images.com/lg/be5e/climbing_helmets_climbing_equipment.jpg',
'https://free-images.com/lg/87ce/lilac_lilac_bush_lilac.jpg'
];
for(let i= 0; i<files.length; i++) {
await downLoadFileAxios(files[i]);
}
}
async function downLoadFileAxios(url) {
let name = url.slice(url.lastIndexOf('/') + 1);
let imagePath = path.resolve(__dirname, "newDir", name);
let writer = fs.createWriteStream(imagePath);
const response = await axios({
url,
method: 'GET',
responseType: 'stream'
})
response.data.pipe(writer)
return new Promise((resolve, reject) => {
writer.on('finish', resolve)
writer.on('error', reject)
})
}
function zipTheFile () {
let zipPath = path.resolve(__dirname, "assets", "file.zip");
return new Promise((resolve, reject) => {
zipDir("newDir", {
saveTo: zipPath
}, function(err, buffer) {
if(err) {
return reject(err);
}
return resolve('done');
});
})
}
Hope this helps!.

Cannot Stub Function Returning Promise

I was trying to stub an arrow function removeUserEntry, but when executing acctRmRouter in the test, my stub seems being ignored. I have to explicitly stub the UserModel's deleteOne method to get the test successfully, I am wondering why the ignorance happens, thank you
acctRoute.js
const removeUserEntry = (username) => {
const condition = {username: username};
return UserModel.deleteOne(condition)
.then((res) => {
if (res.n < 1) {
throw new Error('User not exists');
}
return true;
}, (err) => {throw err})
.catch(err => err);
};
const acctRmRouter = function(httpReq, httpRes, next) {
if (!argValidate(httpReq.body, 'string')) {
httpRes.locals = {api: { success: false }};
// return to avoid running downwards
return next(new Error('bad argument'));
}
// perform DB rm user
return removeUserEntry(httpReq.body).then((res) => {
if (res === true) {
httpRes.locals = {api: { success: true }};
next();
} else {
httpRes.locals = {api: { success: false }}
next(res);
}
});
};
acctRoute.spec.js
it('should remove user handler pass success request', async () => {
shouldDbReset = false;
const mockRequestURL = "/api/account/rm-user";
const mockRequest = httpMocks.createRequest({
method: "POST",
url: mockRequestURL,
headers: {
"Content-Type": "text/plain"
},
body: 'validRmUser',
});
const mockResponse = httpMocks.createResponse();
const spyNext = sinon.spy();
const stubRemoveUserEntry = sinon.stub(accountRouterHelper, 'removeUserEntry');
stubRemoveUserEntry.callsFake(function(){
return Promise.resolve(true);
}); // Expecting this function to be stubbed, and always return true
await accountRouterHelper.acctRmRouter(mockRequest, mockResponse, spyNext);
/* But when running the function, it returns error object with "User not exists"
which is not what intended */
const firstCallArgs = spyNext.getCall(0).args[0];
expect(spyNext.called).to.be.true;
console.log(`firstCallArgs: ${firstCallArgs}`)
expect(firstCallArgs instanceof Error).to.be.false;
expect(spyNext.args[0].length).to.equal(0);
expect(mockResponse.statusCode).to.equal(200);
expect(mockResponse.locals.api.success).to.be.true;
stubRemoveUserEntry.resetHistory();
stubRemoveUserEntry.restore();
});
The following indeed stubbed successfully with similar pattern to removeUserEntry.
acctRoute.js
const createUserEntry = (userData) => {
const updatedUserData = filterInput(userData);
const userDoc = new UserModel(updatedUserData);
return userDoc.save()
.then((userObj) => userObj._doc
,(err) => { throw err;})
.catch(err => err);
};
const acctCreateRouter = function (httpReq, httpRes, next) {
// do something in mongodb
return createUserEntry(userCondition)
.then((response) => {
if (!(response instanceof Error)) {
httpRes.locals = {api: { success: true}};
next();
} else {
httpRes.locals = {api: { success: false}};
next(response);
}
}, (err) => {
httpRes.locals = {api: { success: false}};
next(err);
})
.catch((err) => {
httpRes.locals = {api: { success: false}};
next(err);
});
};
const acctOutputRouter = function(req, res, next) {
if (res.locals) {
res.send(res.locals.api);
} else {next()}
};
acctRoute.spec.js
it("should return and save the success result to response locals for next route", () => {
shouldDbReset = false;
const mockResponse = httpMocks.createResponse();
const stubCreateUserEntry = sinon.stub(accountRouterHelper, 'createUserEntry');
const mockNext = sinon.spy();
stubCreateUserEntry.callsFake(function(){
return Promise.resolve();
}); // Unlike removeUserEntry, stubbing neatly with desired output
return accountRouterHelper.acctCreateRouter(mockRequest, mockResponse, mockNext)
.then(() => {
expect(mockNext.called).to.be.true;
expect(mockResponse.locals.api.success).to.be.true;
})
.finally(() => {
mockNext.resetHistory();
stubCreateUserEntry.restore();
});
});
Issue
sinon.stub(accountRouterHelper, 'removeUserEntry') replaces the module export.
acctRmRouter() is not calling the module export, it is calling removeUserEntry() directly so stubbing the module export does nothing.
Solution
Refactor acctRmRouter() to call the module export for removeUserEntry().
ES6
// import module into itself
import * as self from './acctRoute';
...
const acctRmRouter = function(httpReq, httpRes, next) {
...
// call the function using the module
return self.removeUserEntry(httpReq.body).then((res) => {
...
Node.js module
...
const acctRmRouter = function(httpReq, httpRes, next) {
...
// call the function using module.exports
return module.exports.removeUserEntry(httpReq.body).then((res) => {
...

Find one or create with Mongoose

I have
Page.findById(pageId).then(page => {
const pageId = page.id;
..
});
My problem is that if no page id is given, it should just take the first available page given some conditions, which is done by
Page.findOne({}).then(page => {
const pageId = page.id;
..
});
but if no page is found, it should create a new page and use this, which is done with
Page.create({}).then(page => {
const pageId = page.id;
..
});
But how do I combine all this to as few lines as possible?
I have a lot of logic going on inside
page => { ... }
so I would very much like to do this smart, so I can avoid doing it like this
if (pageId) {
Page.findById(pageId).then(page => {
const pageId = page.id;
..
});
} else {
Page.findOne({}).then(page => {
if (page) {
const pageId = page.id;
..
} else {
Page.create({}).then(page => {
const pageId = page.id;
..
});
}
});
}
I am thinking I maybe could assign a static to the schema with something like
pageSchema.statics.findOneOrCreate = function (condition, doc, callback) {
const self = this;
self.findOne(condition).then(callback).catch((err, result) => {
self.create(doc).then(callback);
});
};
As per the Mongoose docs:
As per previous SO answer
Model.findByIdAndUpdate()
"Finds a matching document, updates it according to the update arg, passing any options, and returns the found document (if any) to the callback."
In the options set upsert to true:
upsert: bool - creates the object if it doesn't exist. defaults to false.
Model.findByIdAndUpdate(id, { $set: { name: 'SOME_VALUE' }}, { upsert: true }, callback)
Related to Yosvel Quintero's answer which didn't work for me:
pageSchema.statics.findOneOrCreate = function findOneOrCreate(condition, callback) {
const self = this
self.findOne(condition, (err, result) => {
return result ? callback(err, result) : self.create(condition, (err, result) => { return callback(err, result) })
})
}
And then use it like:
Page.findOneOrCreate({ key: 'value' }, (err, page) => {
// ... code
console.log(page)
})
Promise async/await version.
Page.static('findOneOrCreate', async function findOneOrCreate(condition, doc) {
const one = await this.findOne(condition);
return one || this.create(doc);
});
Usage
Page.findOneOrCreate({ id: page.id }, page).then(...).catch(...)
Or
async () => {
const yourPage = await Page.findOneOrCreate({ id: page.id }, page);
}
Each Schema can define instance and static methods for its model. Statics are pretty much the same as methods but allow for defining functions that exist directly on your Model
Static method findOneOrCreate:
pageSchema.statics.findOneOrCreate = function findOneOrCreate(condition, doc, callback) {
const self = this;
self.findOne(condition, (err, result) => {
return result
? callback(err, result)
: self.create(doc, (err, result) => {
return callback(err, result);
});
});
};
Now when you have an instance of Page you can call findOneOrCreate:
Page.findOneOrCreate({id: 'somePageId'}, (err, page) => {
console.log(page);
});
One lines solution with async/await:
const page = Page.findOne({}).then(p => p || p.create({})
If you don't want to add a static method to the model, you can try to move some things around and at least not to have all these callback nested levels:
function getPageById (callback) {
Page.findById(pageId).then(page => {
return callback(null, page);
});
}
function getFirstPage(callback) {
Page.findOne({}).then(page => {
if (page) {
return callback(null, page);
}
return callback();
});
}
let retrievePage = getFirstPage;
if (pageId) {
retrievePage = getPageById;
}
retrievePage(function (err, page) {
if (err) {
// #todo: handle the error
}
if (page && page.id) {
pageId = page.id;
} else {
Page.create({}).then(page => {
pageId = page.id;
});
}
});
The solutions posted here ignore that this pattern is most common when there's a unique index on a field or a combination of fields. This solution considers unique index violation errors correctly:
mongoose.plugin((schema) => {
schema.statics.findOrCreate = async function findOrCreate(key, attrs) {
try {
return await this.create({ ...attrs, ...key });
} catch (error) {
const isDuplicateOnThisKey =
error.code === 11000 &&
Object.keys(error.keyPattern).sort().join(',') ===
Object.keys(key).sort().join(',');
if (isDuplicateOnThisKey) {
const doc = await this.findOne(error.keyValue);
doc.set(attrs);
return await doc.save();
}
throw error;
}
};
});
Usage:
await Post.findOrCreate({ slug: 'foobar' }, { title: 'Foo Bar', body });
try this..
var myfunc = function (pageId) {
// check for pageId passed or not
var newId = (typeof pageId == 'undefined') ? {} : {_id:pageId};
Page.findOne(pageId).then(page => {
if (page)
const pageId = page.id;
else { // if record not found, create new
Page.create({}).then(page => {
const pageId = page.id;
});
}
});
}

Categories