I'm trying to create a NodeJS app. Below is code that is supposed to be called when an admin creates a new product. Most of the code works, but I'm having trouble rendering the view only after the rest of the code has executed (the DB functions are asynchronous). I've wrapped much of the code in promises (to make certain blocks execute in the right order) and console logs (to pinpoint problems).
I'd like to point out that the console.dir(rejProducts)just below console.log(111) logs and empty array. Also, adding console.dir(rejProducts) just before the end bracket of the for loop logs an empty array. Thanks! Please let me know if you need more information.
app.post('/products/new', function (req, res, next) {
// Async function: find all categories
Category.find(function(err, categories) {
// Hidden count that tells num products to be created by form
var numProducts = req.body[`form-item-count`];
// Array of all rejected products adds
var rejProducts = [];
var promiseLoopProducts = new Promise(function(resolve, reject) {
var promiseProducts = [];
// Loop through all addded products
for (let i = 0; i < numProducts; i++) {
var promiseProductCheck = new Promise(function(resolve, reject) {
var name = validate.sanitize(req.body[`name_${i}`]);
var category = validate.sanitize(req.body[`category_${i}`]);
var price = validate.sanitize(req.body[`price_${i}`].replace(/\$/g, ""));
var stock = validate.sanitize(req.body[`stock_${i}`]);
var image = validate.sanitize(req.body[`image_${i}`]);
var description = validate.sanitize(req.body[`description_${i}`]);
var rejProduct;
var rejFields = { 'name': name, 'category': category, 'price': price,
'stock': stock, 'image': image,
'description': description };
var rejErrors = {};
var productData = {
name: name,
category: category,
price: price,
stock: stock,
image: image,
description: description
};
var promiseCategoryCheck = new Promise(function(resolve, reject) {
if (ObjectId.isValid(category)) {
var promiseCategoryCount = new Promise(function(resolve, reject) {
Category.count({'_id': category}, function(error, count) {
rejErrors['name'] = validate.isEmpty(name);
if (count == 0) rejErrors['category'] = true;
rejErrors['price'] = !validate.isPrice(price);
rejErrors['stock'] = !validate.isInt(stock);
if( validate.isEmpty(name) || !validate.isPrice(price) ||
count == 0 || !validate.isInt(stock) ) {
rejProduct = { 'fields': rejFields, 'errors': rejErrors };
rejProducts.push(rejProduct);
console.dir(rejProducts);
console.log(count);
return resolve();
}
else {
Product.create(productData, function (error, product) {
if (error) return next(error);
console.log(77);
console.dir(rejProducts);
return resolve();
});
}
if (error) return next(error);
});
});
promiseCategoryCount.then(function() {
console.dir(rejProducts);
return resolve();
});
} else {
rejErrors['category'] = true;
rejProduct = { 'fields': rejFields, 'errors': rejErrors };
rejProducts.push(rejProduct);
console.dir(rejProducts);
}
});
promiseCategoryCheck.then(function() {
console.dir(rejProducts);
promiseProducts.push(promiseProductCheck);
console.log(promiseProductCheck);
console.log(promiseProducts);
return resolve();
});
});
promiseProductCheck.then(function() {
console.log(106);
console.dir(rejProducts);
});
}
Promise.all(promiseProducts).then(function() {
console.log(111);
console.dir(rejProducts); // Empty array!
return resolve();
});
});
promiseLoopProducts.then(function() {
console.log(118);
console.dir(rejProducts); // Empty array!
res.render('products/new', { categories: categories, rejProducts: rejProducts });
});
});
});
Edit: I've made some changes to the code. There it seems like util.promisify is not being recognized as a function. I am using Node 9.4.
module.exports = function(app){
const util = require('util');
require('util.promisify').shim();
const validate = require('../functions/validate');
const Category = require('../db/categories');
const Product = require('../db/products');
var ObjectId = require('mongoose').Types.ObjectId;
//Category.find as function that returns a promise
const findCategories = util.promisify(Category.find);
const countCategories = (categoryId) => {
util.promisify(Category.count({'_id': categoryId}));
};
const bodyToProduct = (body, i) => {
var name = validate.sanitize(body[`name_${i}`]);
var category = validate.sanitize(body[`category_${i}`]);
var price = validate.sanitize(body[`price_${i}`].replace(/\$/g, ""));
var stock = validate.sanitize(body[`stock_${i}`]);
var image = validate.sanitize(body[`image_${i}`]);
var description = validate.sanitize(body[`description_${i}`]);
return {
name: name,
category: category,
price: price,
stock: stock,
image: image,
description: description
};
};
app.post('/products/new', function (req, res, next) {
// Async function: find all categories
return findCategories()
.then(
categories=>{
// Hidden count that tells num products to be created by form
var numProducts = req.body[`form-item-count`];
// Array of all rejected products adds
var rejProducts = [];
return Promise.all(
Array.from(new Array(numProducts),(v,i)=>i)
.map(//map [0...numProducts] to product object
i=>bodyToProduct(req.body,i)
)
.map(
product => {
var rejErrors;
var rejName = validate.isEmpty(name);
var rejCategory;
var rejPrice = !validate.isPrice(price);
var rejStock = !validate.isInt(stock);
if (ObjectId.isValid(product.category)) {
return countCategories()
.then(
count=> {
if (count == 0) rejCategory = true;
if(rejName || rejCategory || rejPrice || rejStock ) {
rejErrors = {
name: rejName,
category: rejCategory,
price: rejPrice,
stock: rejStock
}
rejProduct = { 'fields': product, 'errors': rejErrors };
rejProducts.push(rejProduct);
console.dir(rejProducts);
console.log(count);
} else {
Product.create(productData, function (error, product) {
if (error) return next(error);
console.log(77);
console.dir(rejProducts);
});
}
}
).catch(function() {
console.log("Count function failed.");
});
} else {
rejCategory = true;
rejErrors = {
name: rejName,
category: rejCategory,
price: rejPrice,
stock: rejStock
}
rejProduct = { 'fields': product, 'errors': rejErrors };
rejProducts.push(rejProduct);
console.dir(rejProducts);
}
}
)
).then(function() {
res.render('products/new', { categories: categories, rejProducts: rejProducts });
}).catch(function() {
console.log("Promise all products failed.");
});
}
).catch(function() {
console.log("Find categories failed.");
})
});
}
Some tips: if your function is over 100 lines you may be doing to much in the function.
If you have to get data from your request the way you get products of it then write better client side code (products should be an array of product objects that should not need to be sanitized). Validation is needed on the server because you an never trust what the client is sending you. But looking at the sanitize you don't even trust what your client script is sending you.
Try to write small functions that do a small thing and try to use those.
Use .map to map type a to type b (for example req.body to array of products as in the example code).
Use the result of .map as an argument to Promise.all
Use util.promisify to change a callback function into a function that returns a promise, since you are using an old version of node I've added an implementation of promisify:
var promisify = function(fn) {
return function(){
var args = [].slice.apply(arguments);
return new Promise(
function(resolve,reject){
fn.apply(
null,
args.concat([
function(){
var results = [].slice.apply(arguments);
(results[0])//first argument of callback is error
? reject(results[0])//reject with error
: resolve(results.slice(1,results.length)[0])//resolve with single result
}
])
)
}
);
}
};
module.exports = function(app){
const validate = require('../functions/validate');
const Category = require('../db/categories');
const Product = require('../db/products');
var ObjectId = require('mongoose').Types.ObjectId;
//Category.find as function that returns a promise
const findCategories = promisify(Category.find.bind(Category));
const countCategories = (categoryId) => {
promisify(Category.count.bind(Category))({'_id': categoryId});
};
const createProduct = promisify(Product.create.bind(Product));
const REJECTED = {};
const bodyToProduct = (body, i) => {
var name = validate.sanitize(body[`name_${i}`]);
var category = validate.sanitize(body[`category_${i}`]);
var price = validate.sanitize(body[`price_${i}`].replace(/\$/g, ""));
var stock = validate.sanitize(body[`stock_${i}`]);
var image = validate.sanitize(body[`image_${i}`]);
var description = validate.sanitize(body[`description_${i}`]);
return {
name: name,
category: category,
price: price,
stock: stock,
image: image,
description: description
};
};
const setReject = product => {
var rejErrors;
var rejName = validate.isEmpty(product.name);
var rejCategory;
var rejPrice = !validate.isPrice(product.price);
var rejStock = !validate.isInt(product.stock);
const countPromise = (ObjectId.isValid(product.category))
? countCategories()
: Promise.resolve(0);
return countPromise
.then(
count => {
if (count == 0) rejCategory = true;
if (rejName || rejCategory || rejPrice || rejStock) {
rejErrors = {
type:REJECTED,
name: rejName,
category: rejCategory,
price: rejPrice,
stock: rejStock
}
return rejErrors;
}
return false;
}
);
};
const productWithReject = product =>
Promise.all([
product,
setReject(product)
]);
const saveProductIfNoRejected = ([product,reject]) =>
(reject===false)
? Product.create(product)
.catch(
err=>({
type:REJECTED,
error:err
})
)
: reject;
app.post('/products/new', function (req, res, next) {
// Async function: find all categories
return findCategories()
.then(
categories => {
// Hidden count that tells num products to be created by form
var numProducts = req.body[`form-item-count`];
// Array of all rejected products adds
var rejProducts = [];
return Promise.all(
Array.from(new Array(numProducts), (v, i) => i)
.map(//map [0...numProducts] to product object
i => bodyToProduct(req.body, i)
)
.map(
product=>
productWithReject(product)
.then(saveProductIfNoRejected)
)
).then(
results =>
res.render(
'products/new',
{
categories: categories,
rejProducts: results.filter(x=>(x&&x.type)===REJECTED)
}
)
).catch(
error=>
console.log("Promise all products failed.",error)
);
}
).catch(
error=>
console.log("Find categories failed.",error)
)
});
}
Related
I am trying to write a function that takes into account 3 conditions whenever Stores/{storeId}/{departmentId}/{productId} gets triggered and write new data in ref.child('Home').child('Chiep').child(departmentId).child(productId).
1) When there is no data in firestore, I need to fill up all the fields in Realtime DB, by making queries in 2 different firestore's nodes: Stores and Products in order to take their images.
2) When a change is made in Stores node and it comes from the same {storeId}, I just need to update some data without making any additional query.
3) And finally, when a change is made in Stores node and it comes from other {storeId}, I need to make only one query in the Stores node.
exports.homeChiepest = functions.firestore
.document('Stores/{storeId}/{departmentId}/{productId}')
.onWrite((change, context) => {
const storeId = context.params.storeId;
const departmentId = context.params.departmentId;
const productId = context.params.productId;
const ref = admin.database().ref();
// Get an object with the current document value.
// If the document does not exist, it has been deleted.
const document = change.after.exists ? change.after.data() : null;
// Get an object with the previous document value (for update or delete)
const oldDocument = change.before.exists ? change.before.data() : null;
// Prevent infinite loops
if (!change.after.exists) {
console.log('DATA DELETED RETURN NULL');
return null;
}
const newPrice = document.price;
const newTimestamp = document.timestamp;
return ref.child('Home').child('Chiep')
.child(departmentId).child(productId)
.once('value')
.then(dataSnapshot => {
if (dataSnapshot.val() !== null) {
console.log('CHIEP DOES exist');
const oldPrice = dataSnapshot.val().price;
const storeKey = dataSnapshot.val().storeKey;
if (storeId === storeKey) {
console.log('SAME STORE - Change price and timestamp');
var newChiepest = {
timestamp: newTimestamp,
price: newPrice
};
return dataSnapshot.ref.update(newChiepest);
} else {
console.log('OTHER STORE - Verify if price is chieper...');
if (newPrice <= oldPrice) {
console.log('NEW PRICE: '+newPrice+' is chieper than the older one: '+oldPrice);
return change.after.ref.parent.parent.get().then(doc => { // HERE Avoid nesting promises
newStoreImg = doc.data().image;
var newStoreChiep = {
price: newPrice,
storeImg: newStoreImg,
storeKey: storeId,
timestamp: newTimestamp
};
return dataSnapshot.ref.update(newStoreChiep);
});
} else {
console.log('NEW PRICE: '+newPrice+' is mode EXPENSIVE than the older one: '+oldPrice);
}
return null;
}
} else {
console.log('data does NOT exist, so WRITE IT!');
let getStoreData = change.after.ref.parent.parent.get();
let getProductData = admin.firestore().collection('Products').doc('Departments').collection(departmentId).doc(productId).get();
return Promise.all([getStoreData, getProductData]).then(values => { // HERE Avoid nesting promises
const [store, product] = values;
var newHomeChiepest = {
depId: departmentId,
price: newPrice,
prodImg: product.data().image,
prodKey: productId,
storeKey: storeId,
storeImg: store.data().image,
timestamp: newTimestamp
};
return dataSnapshot.ref.set(newHomeChiepest);
});
}
})
.catch(error => {
console.log('Catch error reading Home: ',departmentId ,'/', productId,'; message: ',error);
return false;
});
});
The problem is: different possibilities of querying or not querying another firestore node led me to a warning while uploading the Clound Function, that is:
warning Avoid nesting promises promise/no-nesting
I appreciate any help to refactor this code.
You could use a variable to manage a "shunting", depending on the different cases, as follows (untested):
exports.homeChiepest = functions.firestore
.document('Stores/{storeId}/{departmentId}/{productId}')
.onWrite((change, context) => {
const storeId = context.params.storeId;
const departmentId = context.params.departmentId;
const productId = context.params.productId;
const ref = admin.database().ref();
const document = change.after.exists ? change.after.data() : null;
// Prevent infinite loops
if (!change.after.exists) {
console.log('DATA DELETED RETURN NULL');
return null;
}
const newPrice = document.price;
const newTimestamp = document.timestamp;
let shunting; // <-- We manage the shunting through this variable
let chiepRef;
return ref.child('Home').child('Chiep')
.child(departmentId).child(productId)
.once('value')
.then(dataSnapshot => {
chiepRef = dataSnapshot.ref;
if (dataSnapshot.val() !== null) {
console.log('CHIEP DOES exist');
const oldPrice = dataSnapshot.val().price;
const storeKey = dataSnapshot.val().storeKey;
if (storeId === storeKey) {
shunting = 1
console.log('SAME STORE - Change price and timestamp');
var newChiepest = {
timestamp: newTimestamp,
price: newPrice
};
return chiepRef.update(newChiepest);
} else {
console.log('OTHER STORE - Verify if price is chieper...');
if (newPrice <= oldPrice) {
console.log('NEW PRICE: ' + newPrice + ' is chieper than the older one: ' + oldPrice);
shunting = 2
return change.after.ref.parent.parent.get();
} else {
console.log('NEW PRICE: ' + newPrice + ' is mode EXPENSIVE than the older one: ' + oldPrice);
shunting = 3
return null;
}
}
} else {
console.log('data does NOT exist, so WRITE IT!');
shunting = 4;
let getStoreData = change.after.ref.parent.parent.get();
let getProductData = admin.firestore().collection('Products').doc('Departments').collection(departmentId).doc(productId).get();
return Promise.all([getStoreData, getProductData])
}
})
.then(result => {
if (shunting === 2) {
const newStoreImg = result.data().image;
var newStoreChiep = {
price: newPrice,
storeImg: newStoreImg,
storeKey: storeId,
timestamp: newTimestamp
};
return chiepRef.update(newStoreChiep);
} else if (shunting === 4) {
const [store, product] = result;
const newHomeChiepest = {
depId: departmentId,
price: newPrice,
prodImg: product.data().image,
prodKey: productId,
storeKey: storeId,
storeImg: store.data().image,
timestamp: newTimestamp
};
return chiepRef.set(newHomeChiepest);
} else {
return null;
}
})
.catch(error => {
console.log('may be adapted, function of shunting', error);
return null;
});
});
The firebase function I'm currently using retrieves data from a certain branch in the database where the value may or may not have percent encoding. The value is a user's username and it's encoded if there's a '.' in the name. When the user gets a notification, it has their name in the body of it, and I'm trying to figure out how to removePercentEncoding if necessary. My cloud function:
exports.newPost = functions.database.ref('/{school}/posts').onWrite((change, context) => {
const school = context.params.school
const postUsername = admin.database().ref('/{school}/lastPost/lastPostUser').once('value')
var db = admin.database();
var val1, val2;
db.ref(`/Herrick Middle School/lastPost/lastPostUser`).once('value').then(snap => {
val1 = snap.val();
console.log(snap.val());
return val1
}).then(() => {
return db.ref("test2/val").once('value');
}).catch(err => {
console.log(err);
});
return loadUsers().then(users => {
let tokens = [];
for (let user of users) {
tokens.push(user.pushToken);
console.log(`pushToken: ${user.pushToken}`);
}
let payload = {
notification: {
title: school,
body: `${val1} just posted something.`,
sound: 'Apex',
badge: '1'
}
};
return admin.messaging().sendToDevice(tokens, payload);
});
});
function loadUsers() {
let dbRef = admin.database().ref('/Herrick Middle School/regisTokens');
let defer = new Promise((resolve, reject) => {
dbRef.once('value', (snap) => {
let data = snap.val();
let users = [];
for (var property in data) {
users.push(data[property]);
console.log(`data: ${property}`);
}
resolve(users);
}, (err) => {
reject(err);
});
});
return defer;
}
More specifically, I was hoping someone could shed some light on how to remove encoding from
val
Thanks in advance.
not sure i understand but either native JS decodeURI() or regex like this
var encoded = "john%doe%doe%bird";
console.log(encoded.replace(/%/g, "."));
As shown bellow, I am pushing the object link_to_json returns into an array allShirts declared in html_to_json.
However, the console.dir on the third last line and the return value of html_to_json logs an array of undefined references. Which I presume is because console.dir and return is executed before link_to_json functions finished.
How do I ensure the return value of html_to_json is a filled up allShirts array?
//Go to individual links and scrape relevant info
const link_to_json = (link) => {
request(link, (err, res, body) => {
if (!error_handler(err, res, link)) {
const $ = cheerio.load(body);
const shirt_detail = $('.shirt-details').find('h1').text();
const Title = shirt_detail.substr(shirt_detail.indexOf(' ') + 1);
const Price = shirt_detail.substr(0, shirt_detail.indexOf(' '));
const ImageURL = $('.shirt-picture').find('img').attr('src');
const URL = link;
return new Shirt(Title, Price, ImageURL, URL);
} else return {};
});
}
//Crawl through all individual links listed in Root
const html_to_json = body => {
const allShirts = [];
const $ = cheerio.load(body);
$('.products').find('a').each((index, val) => {
allShirts.push(link_to_json(rootURL + $(val).attr('href')));
});
console.dir(allShirts); // <--- HERE
return allShirts;
}
There's a few ways to go after this, but I like the Async library for this sort of thing.
How I'd handle your problem is to actually get all the URLs first, so changing your body scrape to something like this instead:
const shirtLinks = [];
$('.products').find('a').each((index, val) => {
shirtLinks.push(rootURL + $(val).attr('href'));
});
You need your conversion function to be asynchronous as well:
const linkToJSON = (link, cb) => {
request(link, (err, res, body) => {
if (!error_handler(err, res, link)) {
const $ = cheerio.load(body);
const shirt_detail = $('.shirt-details').find('h1').text();
const Title = shirt_detail.substr(shirt_detail.indexOf(' ') + 1);
const Price = shirt_detail.substr(0, shirt_detail.indexOf(' '));
const ImageURL = $('.shirt-picture').find('img').attr('src');
const URL = link;
return cb(null, new Shirt(Title, Price, ImageURL, URL));
}
return cb();
});
}
Then use async to map them across the async function that fetches the data:
async.map(shirtLinks, linkToJSON, (err, results) => {
console.dir(results);
});
This is how I would do it. I find it easier to debug this way.
let getShirtDetailsBody = (link) => {
return new Promise((resolve, reject) => {
request(link, (err, res, body) => {
if (err) {
reject(err)
} else {
resolve(body)
}
})
})
}
let getShirt = (body) => {
const $ = cheerio.load(body);
const shirt_detail = $('.shirt-details').find('h1').text();
const Title = shirt_detail.substr(shirt_detail.indexOf(' ') + 1)
const Price = shirt_detail.substr(0, shirt_detail.indexOf(' '))
const ImageURL = $('.shirt-picture').find('img').attr('src')
const URL = link
return new Shirt(Title, Price, ImageURL, URL)
}
let getAllProductsShirtsBody = (body) => {
const $ = cheerio.load(body)
return Promise.all($('.products').find('a').map((index, val) => {
return getShirtDetailsBody(`rootURL${$(val).attr('href')}`)
}))
}
getAllProductsShirtsBody(yourbody).then(allShirtsBody => {
const allShirts = allShirtsBody.map(shirtBody => { return getShirt(shirtBody) })
console.log(allShirts)
}).catch(err => { console.log(err) })
I need to change the value of the array from 'User' to 'Admin' if the function is clicked on and I have to code it in cloud code.
but there is a problem the array does not change
the following code is working but only the part with nameRoleQuery is not working and that's the part which I need to change.
promoteToAdmin: function promoteToAdmin(request, response) {
if (!request.params.companyUser) {
response.error('Request did not have an authenticated user attached with it');
} else {
var companyUser;
var companyUserQuery = new Parse.Query('CompanyUser');
companyUserQuery.include('user');
companyUserQuery.include('company');
companyUserQuery.get(request.params.companyUser, {
useMasterKey: true
}).then((giveRolename) => {
var nameRoleQuery = new Parse.Query(Parse.Role);
request.nameRoleQuery.set('user', ['Admin']);
return nameRoleQuery.save(null, {
useMasterKey: true
});
}).then((companyUserObject) => {
companyUser = companyUserObject;
var userRoleQuery = new Parse.Query(Parse.Role);
userRoleQuery.equalTo('name', 'Company-User-' + companyUser.get('company').id);
return userRoleQuery.first({
useMasterKey: true
});
}).then((userRole) => {
var usersInUserRole = userRole.relation('users');
usersInUserRole.remove(companyUser.get('user'));
return userRole.save(null, {
useMasterKey: true
});
}).then((userRoleSaveResult) => {
var adminRoleQuery = new Parse.Query(Parse.Role);
adminRoleQuery.equalTo('name', 'Company-Admin-' + companyUser.get('company').id);
return adminRoleQuery.first({
useMasterKey: true
});
}).then((adminRole) => {
var usersInAdminRole = adminRole.relation('users');
usersInAdminRole.add(companyUser.get('user'));
return adminRole.save(null,{
useMasterKey: true
});
}).then((saveResult) => {
console.log('after');
response.success('fissa is aan');
}, (error) => {
console.log(error);
});
console.log();
}
}
the role array needs to change.
Still unsure exactly what you're trying to do, but this is what I imagine you want to happen.
CompanyUser has a pointer called 'user' of type Parse.User. You want to update this Parse.User, based on your image, when you run this query. The following should work:
//AS ABOVE
.then((giveRolename) => {
var user = giveRoleName.get("user");
user.remove('role','user');
user.add('role','admin');
return user.save(null, {useMasterKey:true});
}).then((companyUserObject) => {
//companyUserObject is Parse.User object. If CompanyUser object is needed, store in variable beforehand.
I've put below what I would suggest as an improvement to your current code. It's less sequential and provides greater fallback in an error happens in the middle of the process.
It's without arrow functions, so you'll have to change accordingly.
function promoteToAdmin(request, response) {
var params = request.params;
var companyUserId = params["companyUser"];
var companyUser;
return Parse.Promise.as().then(
function()
{
if(!companyUser)
{
return Parse.Promise.error("Request did not have an user attached with it.");
}
var CompanyUser = Parse.Object.extend("CompanyUser");
var companyUserQuery = new Parse.Query(CompanyUser);
companyUserQuery.include("user");
companyUserQuery.include("company");
return companyUserQuery.get(companyUserId,{useMasterKey:true});
}
).then(
function(fetchedCompanyUser)
{
companyUser = fetchedCompanyUser;
var company = companyUser.get("company");
var userRoleQuery = new Parse.Query(Parse.Role);
userRoleQuery.equalTo('name', "Company-User-" + company.id )
var adminRoleQuery = new Parse.Query(Parse.Role);
adminRoleQuery.equalTo('name', "Company-Admin-" + company.id);
return Parse.Promise.when(
userRoleQuery.first({useMasterKey:true}),
adminRoleQuery.first({useMasterKey:true})
)
}
).then(
function(userRole,adminRole)
{
if(!userRole || !adminRole)
{
return Parse.Promise.error("No role found")
}
var user = companyUser.get("user");
user.remove("role","user");
user.add("role","admin");
userRole.getUsers().remove(user);
adminRole.getUsers().add(user);
return Parse.Promise.when(
user.save(null,{useMasterKey:true}),
userRole.save(null,{useMasterKey:true}),
adminRole.save(null,{useMasterKey:true})
)
}
).then(
function(user,userRole,adminRole)
{
response.success("success");
},
function(error)
{
response.error(error);
}
)
}
i'm build an event app, and in my 'Event' schema i've an array of 'Tag's schemas, so each event can have one or more tags.
Event:
var EventSchema = new Schema({
...
tags: [{
type: Schema.Types.ObjectId,
ref: 'Tag'
}],
...
}
And Tag:
var TagSchema = new Schema({
name:{
type: String,
require: true
},
times:{
type: Number,
default: 0
}
});
When a user wants to create an event it sends a json to the /POST in the event middleware with all the information regarding the event and an array composed by
//json sent by client to server
{tags:[{name:tag1},{name:tag2}]
Since two events can't have the same name, in a specific middleware i check if some users has already created the tag or we need to actually store one.
// add the tags
addTags(req, res, next) {
var myBody = req.body;
if (myBody.tags) {
const len = myBody.tags.length
if (len > 0) {
// we need to search and store a tag if is has not already created
for (let i = 0; i < len; i++) {
let currentTag = myBody.tags[i]
// find the currentTag in the DB
Tag.findOne({
name: currentTag.name
}, (err, find) =>{
if (err) return next(err)
// if we not find it
else if (!find) {
// create new one
let newTag = new Tag({
name: myBody.tags[i].name
})
utils.saveModel(newTag, next, (saved) => {
// store it back the ref
req.Event.tags.push(saved._id)
})
} else {
// store the ref
req.Event.tags.push(find._id)
}
})
}
console.log('tags added!.');
next()
}
} else {
next()
}
},
My problem is, how can i call the 'next' only after i've checked all the tags? Is it possible? Thank you
You can use Promise.all to wait for an array of promises to be fulfilled.
Code is untested but should give you the outline of a Promise solution.
mongoose = require('mongoose');
mongoose.Promise = require('bluebird');
// Promise to add a new tag
function addTag(req, currentTag) {
let newTag = new Tag({
name: currentTag.name
})
return newTag.save()
.then( (saved) => {
// Store it back the ref
return req.Event.tags.push(saved._id)
})
}
// Promise to find a tag or add it.
function findTagOrAdd(req, currentTag) {
return Tag.findOne({ name: currentTag.name})
.then( (find) => {
if ( find ) return req.Event.tags.push(find._id);
// Otherwise create new one
return addTag(req, currentTag);
})
}
// Promise to add all tags.
function addTags(req, res, next) {
var myBody = req.body;
if ( ! myBody.tags ) return next();
if ( ! Array.isArray(myBody.tags) ) return next();
if ( myBody.tags.length <= 0 ) return next();
// Promise to find the currentTag in the DB or add it.
var promised_tags = [];
myBody.tags.forEach( (currentTag) => {
promised_tags.push( findTagOrAdd(req, currentTag) )
}
// Wait for all the tags to be found or created.
return Promise.all(promised_tags)
.then( (results) => {
console.log('tags added!.', results);
return next();
})
.catch(next);
}
You probably should use promises, but if you don't want to change your current approach, you can do it the old fashioned way, by counting called callbacks:
function addTags(req, res, next) {
var myBody = req.body
if (!myBody.tags || !myBody.tags.length) {
next()
}
let errorOccured = false
let checkedTags = 0
for (let currentTag of myBody.tags) {
Tag.findOne({ name: currentTag.name }, (err, find) => {
if (errorOccured) {
return
}
if (err) {
errorOccured = true
return next(err)
}
checkedTags += 1
if (!find) {
let newTag = new Tag({ name: currentTag.name })
utils.saveModel(newTag, () => {}, (saved) => {
req.Event.tags.push(saved._id)
if (checkedTags === myBody.tags.length) {
next()
}
})
} else {
req.Event.tags.push(find._id)
if (checkedTags === myBody.tags.length) {
next()
}
}
})
}
}