mongoJS parsing JSON output - node.js

I'm playing with nodeJS and mongoJS. I've successfully queried the DB and returned a set of data:
var databaseUrl = "redirect"; // "username:password#example.com/mydb"
var collections = ["things", "reports"]
var db = require("mongojs").connect(databaseUrl, collections);
db.things.find({url: "google.com"}, function(err, things) {
if( err || !things) {
console.log("URL not Found");
}
else things.forEach( function(RedirectURL) {
console.log(RedirectURL);
} );
});
This returns this:
{ _id: 4fb2c304f2dc05fe12e8c428,
url: 'google.com',
redirect:
[ { url: 'www.goolge.com', weight: 10 },
{ url: 'www.google.co.uk', weight: 20 } ] }
What I need to do now is select certain elements of the array such as redirect.weight or redirect.url.
Does mongoJS allow me to do this easily or is there a better way?
Any pointers greatly appreciated as ever!
Ric

MongoJS implements the mongo API. Calls to document contents use the dot notation.
So in the example above,
var weight = RedirectURL.redirect.0.weight
// equal to 10, since it is the first item of the redirect array.
The same principle can be used with the find commands. So if you wish to find documents with weight = 10, use the following command
db.things.find({redirect.weight: 10})

Related

How to use MongoDB $ne on nested object property

I have a node API which connects to a mongoDB through mongoose. I am creating an advanced results middleware that enabled selecting, filtering, sorting, pagination etc. based on a Brad Traversy course Node.js API Masterclass With Express & MongoDB. This is all good.
I am adapting the code from the course to be able to use the $ne (not equal) operator and I want to be able to get a model that is not equal to a nested property (user id) of the model. I am using this for an explore feature to see a list of things, but I don't want to show the user their own things. I am having trouble figuring out how to access the id property.
********************* UPDATE *********************
It seems all the documentation I've read recommends writing const injected like this:
const injected = {
'user._id': { "$ne": req.user.id }
};
but for some reason it is not working. I can query top level properties that are just a plain string value like this:
const injected = {
access: { "$ne": "public" }
};
but not a property on an object. Does anyone know why? Is it because the property I want to query is an id? I've also tried:
const injected = {
'user._id': { "$ne": mongoose.Types.ObjectId(req.user.id) }
};
which also does not work...
So the model looks like this:
{
name: 'Awesome post',
access: 'public',
user: {
_id: '2425635463456241345', // property I want to access
}
}
then the actual advanced results middleware looks like this and it's the 'injected' object where I am trying to access id. In the course brad uses this syntax to use lte (/?averageCost[lte]=10000) but I do not get any results with my ne. Can anyone help me here?
const advancedResults = (model, populate) => async (req, res, next) => {
let query;
const injected = {
access: 'public',
'user._id[ne]': req.user.id, // I don't think user._id[ne] is correct
};
}
// Copy req.query
const reqQuery = { ...req.query, ...injected };
console.log('injected: ', injected);
// Fields to exclude
const removeFields = ['select', 'sort', 'page', 'limit'];
// Loop over removeFields and delete them from reqQuery
removeFields.forEach(param => delete reqQuery[param]);
// Create query string
let queryStr = JSON.stringify(reqQuery);
// Create operators ($gt, $gte, etc)
queryStr = queryStr.replace(/\b(gt|gte|lt|lte|in|ne)\b/g, match => `$${match}`);
// Finding resource and remove version
query = model.find(JSON.parse(queryStr)).select('-__v');
// Select Fields
if (req.query.select) {
const fields = req.query.select.split(',').join(' ');
query = query.select(fields);
}
// Sort
if (req.query.sort) {
const sortBy = req.query.sort.split(',').join(' ');
query = query.sort(sortBy);
} else {
query = query.sort('-createdAt');
}
// Pagination
const page = parseInt(req.query.page, 10) || 1;
const limit = parseInt(req.query.limit, 10) || 25;
const startIndex = (page - 1) * limit;
const endIndex = page * limit;
const total = await model.countDocuments(JSON.parse(queryStr));
query = query.skip(startIndex).limit(limit);
if (populate) {
query = query.populate(populate);
}
// Executing query
const results = await query;
// Pagination result
const pagination = {};
if (endIndex < total) {
pagination.next = {
page: page + 1,
limit,
};
}
if (startIndex > 0) {
pagination.prev = {
page: page - 1,
limit,
};
}
res.advancedResults = {
success: true,
count: results.length,
pagination,
data: results,
};
next();
};
module.exports = advancedResults;
Answering your question about how to use $ne:
The use of $ne is as follows:
"field":{
"$ne": yourValue
}
Into your query should be like:
"user._id": {
"$ne": req.user.id
}
Example here
$ne operator will return all document where the field value don't match with the given value.
As you have done, to acces the nested field is necessary use the dot notation.
Also, to ensure it works, if your schema defines _id as ObjectId maybe is necessary parse req.user.id to ObjectId.
But if in your schema is a string then should works.
So try (not tested at all):
const injected = {
'user._id': { "$ne": req.user.id }
};

How to update collection value

Assume I have db as;
{ name: "alex" , id: "1"}
I want to update the collection. I want to add "Mr." to the value in name field.
{ name: "Mr.alex" , id: "1"}
How can I do this? Should I wrote 2 query as;
db.collection("user").find({id : "1"}).toArray(function(err, result){
var name = result[0].name;
db.collection("user").updateOne({id : "1"}, {name: "Mr." + name},function(err, result){
})
})
Isn't there any better way to do this as x = x+1 in mongodb?
AFAIK there would be two queries, update operator won't take any field's value and need provided value.
However if you need to do it for all the document or large amount of document, you can write a script and use cursor forEach and for each document change the name and call db.user.save() by passing the argument user object.
Bottom line remains same.
try below:
var cursor = db.users.find({});
while (cursor.hasNext()) {
var user = cursor.next();
user.name = "Mr." + user.name;
user.save();
}
Not possible using one query. You have to iterate through the documents and save them with updated result.
Currently there is no way to reference the retrieved document in the same query, which would allow you to find and update a document within the same operation.
Thus, you will have to make multiple queries to accomplish what you are looking for:
/*
* Single Document
*/
// I'm assuming the id field is unique and can only return one document
var doc = db.collection('user').findOne({ id: '1' });
try {
db.collection('user').updateOne({ id: '1' }, { $set: { name: 'Mr. ' + doc.name }});
} catch (e) {
print(e);
}
If you want to handle multiple update operations, you can do so by using a Bulk() operations builder:
/*
* Multiple Documents
*/
var bulk = db.collection('user').initializeUnorderedBulkOp();
// .forEach is synchronous in MongoDB, as it performs I/O operations
var users = db.collection('user').find({ id: { $gt: 1 } }).forEach(function(user) {
bulk.find({ id: user.id }).update({ $set: { name: 'Mr. ' + user.name }});
});
bulk.execute();
The key to updating the collection with the existing field is to loop through the array returned from the find().toarray() cursor method and update your collection using the Bulk API which allows you to send many update operations within a single request (as a batch).
Let's see with some examples how this pens out:
a) For MongoDB server version 3.2 and above
db.collection("user").find({id : "1"}).toArray(function(err, result){
var operations = [];
result.forEach(function(doc){
operations.push({
"updateOne": {
"filter": {
"_id": doc._id,
"name": doc.name
},
"update": {
"$set": { "name": "Mr." + doc.name }
}
}
});
// Send once in 500 requests only
if (operations.length % 500 === 0 ) {
db.collection("user").bulkWrite(operations, function(err, r) {
// do something with result
}
operations = [];
}
});
// Clear remaining queue
if (operations.length > 0) {
db.collection("user").bulkWrite(operations, function(err, r) {
// do something with result
}
}
})
In the above, you initialise your operations array which would be used by the Bulk API's bulkWrite() function and holds the update operations.
The result from the find().toarray() cursor function is then iterated to create the operations array with the update objects. The operations are limited to batches of 500.
The reason of choosing a lower value than the default batch limit of 1000 is generally a controlled choice. As noted in the documentation there, MongoDB by default will send to the server in batches of 1000 operations at a time at maximum and there is no guarantee that makes sure that these default 1000 operations requests actually fit under the 16MB BSON limit.
So you would still need to be on the "safe" side and impose a lower batch size that you can only effectively manage so that it totals less than the data limit in size when sending to the server.
a) If using MongoDB v3.0 or below:
// Get the collection
var col = db.collection('user');
// Initialize the unordered Batch
var batch = col.initializeUnorderedBulkOp();
// Initialize counter
var counter = 0;
col.find({id : "1"}).toArray(function(err, result){
result.forEach(function(doc) {
batch.find({
"_id": doc._id,
"name": doc.name
}).updateOne({
"$set": { "name": "Mr. " + doc.name }
});
counter++;
if (counter % 500 === 0) {
batch.execute(function(err, r) {
// do something with result
});
// Re-initialize batch
batch = col.initializeOrderedBulkOp();
}
});
if (counter % 1000 != 0 ){
batch.execute(function(err, r) {
// do something with result
});
}
});

mongoose recursive populate

I have been searching for a while and I didn't find any good answer. I have n-deep tree that I am storing in DB and I would like to populate all parents so in the end I get the full tree
node
-parent
-parent
.
.
-parent
So far I populate to level 2, and as I mentioned I need to get to level n.
Node.find().populate('parent').exec(function (err, items) {
if (!err) {
Node.populate(items, {path: 'parent.parent'}, function (err, data) {
return res.send(data);
});
} else {
res.statusCode = code;
return res.send(err.message);
}
});
you can do this now (with https://www.mongodb.com/blog/post/introducing-version-40-mongoose-nodejs-odm)
var mongoose = require('mongoose');
// mongoose.Promise = require('bluebird'); // it should work with native Promise
mongoose.connect('mongodb://......');
var NodeSchema = new mongoose.Schema({
children: [{type: mongoose.Schema.Types.ObjectId, ref: 'Node'}],
name: String
});
var autoPopulateChildren = function(next) {
this.populate('children');
next();
};
NodeSchema
.pre('findOne', autoPopulateChildren)
.pre('find', autoPopulateChildren)
var Node = mongoose.model('Node', NodeSchema)
var root=new Node({name:'1'})
var header=new Node({name:'2'})
var main=new Node({name:'3'})
var foo=new Node({name:'foo'})
var bar=new Node({name:'bar'})
root.children=[header, main]
main.children=[foo, bar]
Node.remove({})
.then(Promise.all([foo, bar, header, main, root].map(p=>p.save())))
.then(_=>Node.findOne({name:'1'}))
.then(r=>console.log(r.children[1].children[0].name)) // foo
simple alternative, without Mongoose:
function upsert(coll, o){ // takes object returns ids inserted
if (o.children){
return Promise.all(o.children.map(i=>upsert(coll,i)))
.then(children=>Object.assign(o, {children})) // replace the objects children by their mongo ids
.then(o=>coll.insertOne(o))
.then(r=>r.insertedId);
} else {
return coll.insertOne(o)
.then(r=>r.insertedId);
}
}
var root = {
name: '1',
children: [
{
name: '2'
},
{
name: '3',
children: [
{
name: 'foo'
},
{
name: 'bar'
}
]
}
]
}
upsert(mycoll, root)
const populateChildren = (coll, _id) => // takes a collection and a document id and returns this document fully nested with its children
coll.findOne({_id})
.then(function(o){
if (!o.children) return o;
return Promise.all(o.children.map(i=>populateChildren(coll,i)))
.then(children=>Object.assign(o, {children}))
});
const populateParents = (coll, _id) => // takes a collection and a document id and returns this document fully nested with its parents, that's more what OP wanted
coll.findOne({_id})
.then(function(o){
if (!o.parent) return o;
return populateParents(coll, o.parent))) // o.parent should be an id
.then(parent => Object.assign(o, {parent})) // replace that id with the document
});
Another approach is to take advantage of the fact that Model.populate() returns a promise, and that you can fulfill a promise with another promise.
You can recursively populate the node in question via:
Node.findOne({ "_id": req.params.id }, function(err, node) {
populateParents(node).then(function(){
// Do something with node
});
});
populateParents could look like the following:
var Promise = require('bluebird');
function populateParents(node) {
return Node.populate(node, { path: "parent" }).then(function(node) {
return node.parent ? populateParents(node.parent) : Promise.fulfill(node);
});
}
It's not the most performant approach, but if your N is small this would work.
Now with Mongoose 4 this can be done. Now you can recurse deeper than a single level.
Example
User.findOne({ userId: userId })
.populate({
path: 'enrollments.course',
populate: {
path: 'playlists',
model: 'Playlist',
populate: {
path: 'videos',
model: 'Video'
}
}
})
.populate('degrees')
.exec()
You can find the official documentation for Mongoose Deep Populate from here.
Just don't :)
There is no good way to do that. Even if you do some map-reduce, it will have terrible performance and problems with sharding if you have it or will ever need it.
Mongo as NoSQL database is really great for storing tree documents. You can store whole tree and then use map-reduce to get some particular leafs from it if you don't have a lot of "find particular leaf" queries. If this doesn't work for you, go with two collections:
Simplified tree structure: {_id: "tree1", tree: {1: [2, {3: [4, {5: 6}, 7]}]}}. Numbers are just IDs of nodes. This way you'll get whole document in one query. Then you just extract all ids and run second query.
Nodes: {_id: 1, data: "something"}, {_id: 2, data: "something else"}.
Then you can write simple recurring function which will replace node ids from first collection with data from second. 2 queries and simple client-side processing.
Small update:
You can extend second collection to be a little more flexible:
{_id: 2, data: "something", children:[3, 7], parents: [1, 12, 13]}
This way you'll be able to start your search from any leaf. And then, use map-reduce to get to the top or to the bottom of this part of tree.
This is a more straight forward approach to caub's answer and great solution. I found it a bit hard to make sense of at first so I put this version together.
Important, you need both 'findOne' and 'find' middleware hooks in place for this solution to work. *
* Also, the model definition must come after the middleware definition *
const mongoose = require('mongoose');
const NodeSchema = new mongoose.Schema({
children: [mongoose.Schema.Types.ObjectId],
name: String
});
const autoPopulateChildren = function (next) {
this.populate('children');
next();
};
NodeSchema
.pre('findOne', autoPopulateChildren)
.pre('find', autoPopulateChildren)
const Node = mongoose.model('Node', NodeSchema)
const root = new Node({ name: '1' })
const main = new Node({ name: '3' })
const foo = new Node({ name: 'foo' })
root.children = [main]
main.children = [foo]
mongoose.connect('mongodb://localhost:27017/try', { useNewUrlParser: true }, async () => {
await Node.remove({});
await foo.save();
await main.save();
await root.save();
const result = await Node.findOne({ name: '1' });
console.log(result.children[0].children[0].name);
});
I tried #fzembow's solution but it seemed to return the object from the deepest populated path. In my case I needed to recursively populate an object, but then return the very same object. I did it like that:
// Schema definition
const NodeSchema = new Schema({
name: { type: String, unique: true, required: true },
parent: { type: Schema.Types.ObjectId, ref: 'Node' },
});
const Node = mongoose.model('Node', NodeSchema);
// method
const Promise = require('bluebird');
const recursivelyPopulatePath = (entry, path) => {
if (entry[path]) {
return Node.findById(entry[path])
.then((foundPath) => {
return recursivelyPopulatePath(foundPath, path)
.then((populatedFoundPath) => {
entry[path] = populatedFoundPath;
return Promise.resolve(entry);
});
});
}
return Promise.resolve(entry);
};
//sample usage
Node.findOne({ name: 'someName' })
.then((category) => {
if (category) {
recursivelyPopulatePath(category, 'parent')
.then((populatedNode) => {
// ^^^^^^^^^^^^^^^^^ here is your object but populated recursively
});
} else {
...
}
})
Beware it's not very efficient. If you need to run such query often or at deep levels, then you should rethink your design
Maybe a lot late for that but mongoose has some documentation on this :
Ancestors Tree Array
Materialized Path Tree Array
I think the first one is more appropriate to you as you are looking to populate parents.
With that solution, you can with one regex query, search all the documents matching your designered output tree.
You would setup documents with this Schema :
Tree: {
name: String,
path: String
}
Paths field would be the absolute path in your tree :
/mens
/mens/shoes
/mens/shoes/boots
/womens
/womens/shoes
/womens/shoes/boots
For example you could search all the childrens of your node '/mens/shoes' with one query :
await Tree.find({ path: /^\/mens/shoes })
It would return all the documents where the path starts with /mens/shoes :
/mens/shoes
/mens/shoes/boots
Then you'd only need some client-side logic to arrange it in a tree structure (a map-reduce)

Node.js: arrays in query

There is the request: /test?strategy[]=12&strategy[]=23
In PHP we may receive array with this code:
$_GET['strategy']
It will be array even if it has 1 param.
In JS we can use this code:
var query = url.parse(request.url, true).query;
var strategy = query['strategy[]'] || [];
But if there will be 1 param strategy( /test?strategy[]=12), it will not be an array.
I think it's a dirty solution to check count of elements and convert strategy to [strategy ] if it equals to 1.
How can I get an array from query in Node.js?
Node itself doesn't parse those types of arrays - nor it does with what PHP calls "associative arrays", or JSON objects (parse a[key]=val to { a: { key: 'val' } }).
For such problem, you should use the package qs to solve:
var qs = require("qs");
qs.parse('user[name][first]=Tobi&user[email]=tobi#learnboost.com');
// gives you { user: { name: { first: 'Tobi' }, email: 'tobi#learnboost.com' } }
qs.parse("user[]=admin&user[]=chuckNorris");
qs.parse("user=admin&user=chuckNorris");
// both give you { user: [ 'admin', 'chuckNorris' ] }
// The inverse way is also possible:
qs.stringify({ user: { name: 'Tobi', email: 'tobi#learnboost.com' }})
// gives you user[name]=Tobi&user[email]=tobi%40learnboost.com

mongoose findOneAndUpdate callback with array of ids

I am using findOneAndUpdate in a forEach loop to create/update multiple entries.
I would like it to return an array of all the object id's it has created or updated.
During the loop, I can see it adding data to the array, but one it leaves the loop, the array is empty.
Should the array not be populated?
here is my code.
var softwareArray = ["Software1","Software2","Software3"],
updatedArray = [];
softwareArray.forEach(function(software){
Software.findOneAndUpdate(
{
Name: software
},
{
Name: software
},
{upsert:true},
function(err, rows){
updatedArray.push(rows._id);
console.log(updatedArray); //This has data in it....
}
);
});
console.log(updatedArray); //This has no data in it...
Edit: Updated with my working changes for Thiago
var softwareArray = ["Software1","Software2","Software3"],
updatedArray = [];
loopSoftware(softwareArray, function(updatedArray){
console.log(updatedArray);
//carry on....
}
function loopSoftware(input, cb){
var returnData = [];
var runLoop = function(software, done) {
Software.findOneAndUpdate(
{Name: software},
{Name: software},
{upsert:true},function(err, rows){
returnData.push(rows._id);
done()
}
);
};
var doneLoop = function(err) {
cb(returnData);
};
async.forEachSeries(input, runLoop, doneLoop);
}
I decorated your code to make you see when what is happening:
var softwareArray = ["Software1","Software2","Software3"],
updatedArray = [];
// TIMESTAMP: 0
softwareArray.forEach(function(software){
// TIMESTAMP: 1, 2, 3
Software.findOneAndUpdate(
{
Name: software
},
{
Name: software
},
{upsert:true},
function(err, rows){
// TIMESTAMP: 5, 6, 7
updatedArray.push(rows._id);
console.log(updatedArray); // This has data in it....
// want to use the result?
if (updatedArray.length == softwareArray.length) {
console.log(updatedArray);
}
}
);
});
// TIMESTAMP: 4
console.log(updatedArray);
Of course this will happen - just like any other networking on Node, it's asynchronous!
This means that the callback you specified for your findOneAndUpdate operation have not run yet when it reaches the console.log(updatedArray); code.
Take a look at Q for working around this common problem.

Resources