Mongo + check multiple fields existing - node.js

I am working mongo with nodejs.
I have array list:
var checkFields = ["field1","field2","field3"];
I try to get the count of records having the array list fields and user field is equal to admin.
Sample data:
[
{
"checkFields": {
"field1": "00124b3a5c31",
"user": "admin"
}
},
{
"checkFields": {
"field2": "00124b3a5c31",
"user": "admin"
}
},
{
"checkFields": {
"field1": "00124b3a5c31",
"user": "regular"
}
}
]
Query:
db.collection_name.find(
{"checkFields.user" : "admin"}
{ "checkFields.field1": { $exists: true} }
)
Expected Result:
Result is to get rows of count of matching the field in array list(checkFields).

Building up an $or array for the list of field existence checks is the right approach, but assuming you're on a current node.js build you can simplify the query creation to:
var checkFieldsLists = checkFields.map(field => ({
['checkFields.' + field]: {$exists: true}
}));
var query = {
$or: checkFieldsLists,
'checkFields.user': 'admin'
}
This removes the superfluous $or for the "user is admin" check which lets you also remove the outer $and, so that the generated query is:
{ '$or':
[ { 'checkFields.field1': { '$exists': true } },
{ 'checkFields.field2': { '$exists': true } },
{ 'checkFields.field3': { '$exists': true } } ],
'checkFields.user': 'admin' }

I tried the following code. Its working but don't know its good solution and perfomance. Please anyone have better answer means please post it.
var checkFields = ["field1", "field2", "field3"];
var checkFieldsLists = [];
for ( i = 0; i < checkFields.length; i++) {
var jsObj = {};
jsObj['checkFields.' + checkFields[i]] = {};
jsObj['checkFields.' + checkFields[i]].$exists = true;
checkFieldsLists.push(jsObj);
}
var query = {
"$and" : [{
"$or" : checkFieldsLists
}, {
"$or" : [{
"checkFields.user" : "admin"
}]
}]
};
console.log(JSON.stringify(query));
//console log will return
/*
{"$and":[{
"$or" : [{
"checkFields.field1" : {
"$exists" : true
}
}, {
"checkFields.field2" : {
"$exists" : true
}
}, {
"checkFields.field3" : {
"$exists" : true
}
}]
}, {
"$or" : [{
"checkFields.user" : "admin"
}]
}]
}
*/
collection.find(query);

Here is the solution using aggregate query.
var Db = require('mongodb').Db, Server = require('mongodb').Server, assert = require('assert');
var db = new Db('localhost', new Server('localhost', 27017));
var checkFields = ["field1", "field2", "field3"];
var checkFieldsLists = [];
for (var i = 0; i < checkFields.length; i++) {
var jsObj = {};
jsObj['checkFields.' + checkFields[i]] = {};
jsObj['checkFields.' + checkFields[i]].$exists = true;
checkFieldsLists.push(jsObj);
}
var query = {
"$and" : [{
"$or" : checkFieldsLists
}, {
"$or" : [{
"checkFields.user" : "admin"
}]
}]
};
var matchQuery = {
"$match" : {
"checkFields.user" : "admin",
"$or" : checkFieldsLists
}
};
var groupQuery = {
$group : {
_id : null,
count : {
$sum : 1
}
}
};
var aggregateCheckFields = function(db, callback) {
console.log("Match query is ====>" + JSON.stringify(matchQuery));
console.log("Group query is ====>" + JSON.stringify(matchQuery));
db.collection('checkfields').aggregate([ matchQuery, groupQuery ]).toArray(
function(err, result) {
assert.equal(err, null);
console.log("Result is ===>" + JSON.stringify(result));
if (result.length > 0) {
console.log("Count is ===>" + result[0].count);
}
callback(result);
});
};
db.open(function(err, db) {
aggregateCheckFields(db, function() {
db.close();
});
});
Output:-
Result is ===>[{"_id":null,"count":3}]
Count is ===>3

Related

How to pass the timestamp in the query param of NodeJS (Express + Mongoose)

I have MongoDB command like this:
db.transaction.aggregate([
{
"$match":
{
$and:[{
'is_deleted': false,
'createdAt':{'$gte': ISODate('2020-09-01T00:00:00.000Z'),'$lte':ISODate('2020-12-15T00:00:00.000Z')},
'type': 'deposit'
}
]
}
},
{
$group: {
_id: null ,
depositRevenue: {$sum: "$amount"}
}
}
]).pretty();
And the output like this:
{
"_id" : null,
"depositRevenue" : 324
}
The query is fine, but when I pass in NodeJS it's not work.
Bellow is my code in ExpressJS and Mongoose.
let revenueData = await Transaction.aggregate([
{
$match: {
$and: [{objFind}]
}
},
{
$group: {
_id: null,
totalRevenue: {$sum: "$amount"}
}
}
]);
with objFind like this:
var startDate = new Date(req.query.startDate).toISOString();
var endDate = new Date(req.query.endDate).toISOString();
var objFind = {};
objFind["is_deleted"] = false;
if(startDate != undefined || endDate != undefined){
objFind["createdAt"] = {};
if(startDate != undefined){
objFind["createdAt"]["$gte"] = startDate;
}
if(endDate != undefined){
objFind["createdAt"]["$lte"] = endDate;
}
}
objFind['type'] = 'deposit';
Please take a look. Thanks you
You need to simplify your code,
create an object with required conditions
var objFind = {
is_deleted: false,
type: 'deposit'
};
check start date and end date available
if (req.query.startDate || req.query.endDate) {
objFind.createdAt = {};
if (req.query.startDate) objFind.createdAt.$gte = new Date(req.query.startDate);
if (req.query.endDate) objFind.createdAt.$lte = new Date(req.query.endDate);
}
by default root fields condition would be and condition, so no need to use $and
let revenueData = await Transaction.aggregate([
{ $match: objFind },
{
$group: {
_id: null,
totalRevenue: { $sum: "$amount" }
}
}
]);

how to update an object of an element in array in mongodb?

This is the structure i have, i want to update the nested array element if an object key matches for example - i want to match grnno :"10431000" and update the other keys of that object like vehicle_no,invoice_no etc.
{
"_id" : ObjectId("5f128b8aeb27bb63057e3887"),
"requirements" : [
{
"grns" : [
{
"invoice_no" : "123",
"vehicle_no" : "345",
"req_id" : "5f128c6deb27bb63057e388a",
"grnno" : "10431000"
},
{
"invoice_no" : "abc",
"vehicle_no" : "def",
"req_id" : "5f128c6deb27bb63057e388a",
"grnno" : "10431001"
}
]
}
]
}
I have tried this code
db.po_grn.update({
"requirements.grns.grnno":"10431001"
}, {
$set: {
"requirements.$.grns": {"invoice_no":"test",vehicle_no:"5455"}
}
})
But this is changing the structure i have like this
"requirements" : [
{
"grns" : {
"invoice_no" : "test",
"vehicle_no":"5455"
},
"req_id" : ObjectId("5f128b8aeb27bb63057e3886")
}
],
grns key should be array, and update should be of the particular object which matches the key "grnno". Please help me out. Thanks.
==Edit==
var grnno = req.body.grnno;
db.po_grn.find({
"requirements.grns.grnno":grnno
}).toArray(function(err, po_grn) {
console.log("po_grn",po_grn);
if (po_grn.length > 0) {
console.log("data.grn.grnno ", grnno);
var query = {
requirements: {
$elemMatch: {
"grns.grnno": grnno
}
}
};
var update = {
$set: {
'requirements.$[].grns.$[inner].invoice_no': data.invoice_no,
'requirements.$[].grns.$[inner].vehicle_no': data.vehicle_no,
}
};
var options = {
arrayFilters: [
{ "inner.grnno" : grnno }
]
};
db.po_grn.update(query, update, options
, function(er, grn) {
console.log("grn",grn,"er",er)
res.send({
status: 1,
message: "Grn updated successfully"
});
}
);
} else {
res.send({
status: 0,
message: "Grn not found "
});
}
})
Use a combination of $[] positional-all operator with array filters to update your inner nested document.
var query = {
requirements: {
$elemMatch: {
"grns.grnno": "10431001"
}
}
};
var update = {
$set: {
'requirements.$[].grns.$[inner].invoice_no': "test",
'requirements.$[].grns.$[inner].vehicle_no': "5455",
}
};
var options = {
arrayFilters: [
{ "inner.grnno" : "10431001" }
]
};
db.collection.update(query, update, options);
Update -
NodeJS native MongoDb driver code attached, which is working fine
const { MongoClient } = require('mongodb');
const url = "mongodb://localhost:27017/";
MongoClient.connect(url, function(err, db) {
if (err) {
throw err;
}
const dbo = db.db("test");
(async() => {
const query = {
requirements: {
$elemMatch: {
"grns.grnno": "10431001"
}
}
};
const update = {
$set: {
'requirements.$[].grns.$[inner].invoice_no': "test",
'requirements.$[].grns.$[inner].vehicle_no': "5455",
}
};
const options = {
arrayFilters: [
{ "inner.grnno" : "10431001" }
],
multi: true
};
try {
const updateResult = await dbo.collection("collection").update(query, update, options);
} catch (err) {
console.error(err);
}
db.close();
})();
});

Use variable in mongodb object dot notation

I want to increment a property value of an object if it does exist inside an array.
Mongo record:
{
"_id" : ObjectId("5b7bdd9f0465e8345ba83aad"),
"userID" : "400",
"userName" : "Jon Snow",
"pageName" : "1",
"courseName" : "Maths",
"socketID" : [
"aswKWYyE1euk2GNIAAAD"
],
"online" : true,
"userHistory" : {
"pagesVisited" : [
{
"page" : "1",
"timesVisited" : 1
}
],
"coursesVisited" : [
"Maths"
]
},
"date" : ISODate("2018-08-21T09:38:39.281Z")
}
Here on userHistory.pagesVisited on the page property if I get the value 1 again then I want to increment the timesVisited property like so:
"pagesVisited" : [
{
"page" : "1",
"timesVisited" : 2
}
],
Here's what I have tried with no luck:
let userDetails = {
userID: queryUser.userID,
userName: queryUser.username,
pageName: queryUser.pageName,
courseName: queryUser.courseName,
socketID: [socket.id],
online: true,
userHistory: {
pagesVisited: [
{
"page" : queryUser.pageName,
"timesVisited" : 1
}
],
coursesVisited: [queryUser.courseName]
},
date: new Date()
};
onlineUsersDB.findOne({'userID': userDetails.userID}).then(async (user) => {
if (user) {
let page = {"page": queryUser.pageName, "timesVisited": 1};
let course = queryUser.courseName;
let updatedUser = await onlineUsersDB.findOneAndUpdate(
{'userID': user.userID},
{
$set: {'online': true},
$push: { 'socketID': socket.id },
},
{ $addToSet: { 'userHistory.coursesVisited': course } },
{ returnOriginal: false }
);
let updatedUserPageRef = updatedUser.value.userHistory.pagesVisited;
if (updatedUserPageRef) {
let pageFound = await updatedUserPageRef.findIndex(item => item.page === page);
if (pageFound >= 0) {
let updatedUserPage = await onlineUsersDB.findOneAndUpdate(
{'userID': updatedUser.value.userID},
// Here I want to reference the variable pageFound
{$inc: { 'userHistory.pagesVisited.[pageFound].timesVisited': 1 }},
{ returnOriginal: false }
);
console.log(JSON.stringify(updatedUserPage,null, 2));
}
}
let users = await onlineUsersDB.find({'online': true}).toArray();
io.to(room).emit('online-users', users);
io.to(room).emit('user-back-online', updatedUser);
} else {
if (userDetails.userID !== '100') {
await onlineUsersDB.insert(userDetails);
}
let users = await onlineUsersDB.find({'online': true}).toArray();
io.to(room).emit('online-users', users);
}
}).catch((e) => console.log(e));
In the above code where my comment is I want to reference the variable pageFound in my object dot notation like so:
{$inc: { 'userHistory.pagesVisited.[pageFound].timesVisited': 1 }}
It works when I give it a hardcoded value like:
{$inc: { 'userHistory.pagesVisited.0.timesVisited': 1 }}
After experimenting a little bit I made it to work like this:
I broke out my query string into another variable using template literals.
let pageInc = `userHistory.pagesVisited.${pageFound}.timesVisited`;
And then referenced the variable in my query like so:
{$inc: { [pageInc]: 1 }}
And it works now.

$addToSet Based on Object key exists

I have array 'pets': [{'fido': ['abc']} that is a embeded document. When I add a pet to the array, how can I check to see if that pet already exists? For instance, if I added fido again... how can I check if only fido exists and not add it? I was hoping I could use $addToSet but I only want to check part of the set(the pets name).
User.prototype.updatePetArray = function(userId, petName) {
userId = { _id: ObjectId(userId) };
return this.collection.findOneAndUpdate(userId,
{ $addToSet: { pets: { [petName]: [] } } },
{ returnOriginal: false,
maxTimeMS: QUERY_TIME });
Result of adding fido twice:
{u'lastErrorObject': {u'updatedExisting': True, u'n': 1}, u'ok': 1, u'value': {u'username': u'bob123', u'_id': u'56d5fc8381c9c28b3056f794', u'location': u'AT', u'pets': [{u'fido': []}]}}
{u'lastErrorObject': {u'updatedExisting': True, u'n': 1}, u'ok': 1, u'value': {u'username': u'bob123', u'_id': u'56d5fc8381c9c28b3056f794', u'location': u'AT', u'pets': [{u'fido': [u'abc']}, {u'fido': []}]}}
If there is always going to be "variable" content within each member of the "pets" array ( i.e petName as the key ) then $addToSet is not for you. At least not not at the array level where you are looking to apply it.
Instead you basically need an $exists test on the "key" of the document being contained in the array, then either $addToSet to the "contained" array of that matched key with the positional $ operator, or where the "key" was not matched then $push directly to the "pets" array, with the new inner content directly as the sole array member.
So if you can live with not returning the modified document, then "Bulk" operations are for you. In modern drivers with bulkWrite():
User.prototype.updatePetArray = function(userId, petName, content) {
var filter1 = { "_id": ObjectId(userId) },
filter2 = { "_id": ObjectId(userId) },
update1 = { "$addToSet": {} },
update2 = { "$push": { "pets": {} } };
filter1["pets." + petName] = { "$exists": true };
filter2["pets." + petName] = { "$exists": false };
var setter1 = {};
setter1["pets.$." + petName] = content;
update1["$addToSet"] = setter1;
var setter2 = {};
setter2[petName] = [content];
update2["$push"]["pets"] = setter2;
// Return the promise that yields the BulkWriteResult of both calls
return this.collection.bulkWrite([
{ "updateOne": {
"filter": filter1,
"update": update1
}},
{ "updateOne": {
"filter": filter2,
"update": update2
}}
]);
};
If you must return the modified document, then you are going to need to resolve each call and return the one that actually matched something:
User.prototype.updatePetArray = function(userId, petName, content) {
var filter1 = { "_id": ObjectId(userId) },
filter2 = { "_id": ObjectId(userId) },
update1 = { "$addToSet": {} },
update2 = { "$push": { "pets": {} } };
filter1["pets." + petName] = { "$exists": true };
filter2["pets." + petName] = { "$exists": false };
var setter1 = {};
setter1["pets.$." + petName] = content;
update1["$addToSet"] = setter1;
var setter2 = {};
setter2[petName] = [content];
update2["$push"]["pets"] = setter2;
// Return the promise that returns the result that matched and modified
return new Promise(function(resolve,reject) {
var operations = [
this.collection.findOneAndUpdate(filter1,update1,{ "returnOriginal": false}),
this.collection.findOneAndUpdate(filter2,update2,{ "returnOriginal": false})
];
// Promise.all runs both, and discard the null document
Promise.all(operations).then(function(result) {
resolve(result.filter(function(el) { return el.value != null } )[0].value);
},reject);
});
};
In either case this requires "two" update attempts where only "one" will actually succeed and modify the document, since only one of the $exists tests is going to be true.
So as an example of that first case, the "query" and "update" are resolving after interpolation as:
{
"_id": ObjectId("56d7b759e955e2812c6c8c1b"),
"pets.fido": { "$exists": true }
},
{ "$addToSet": { "pets.$.fido": "ccc" } }
And the second update as:
{
"_id": ObjectId("56d7b759e955e2812c6c8c1b"),
"pets.fido": { "$exists": false }
},
{ "$push": { "pets": { "fido": ["ccc"] } } }
Given varibles of:
userId = "56d7b759e955e2812c6c8c1b",
petName = "fido",
content = "ccc";
Personally I would not be naming keys like this, but rather change the structure to:
{
"_id": ObjectId("56d7b759e955e2812c6c8c1b"),
"pets": [{ "name": "fido", "data": ["abc"] }]
}
That makes the update statements easier, and without the need for variable interpolation into the key names. For example:
{
"_id": ObjectId(userId),
"pets.name": petName
},
{ "$addToSet": { "pets.$.data": content } }
and:
{
"_id": ObjectId(userId),
"pets.name": { "$ne": petName }
},
{ "$push": { "pets": { "name": petName, "data": [content] } } }
Which feels a whole lot cleaner and can actually use an "index" for matching, which of course $exists simply cannot.
There is of course more overhead if using .findOneAndUpdate(), since this is afterall "two" actual calls to the server for which you need to await a response as opposed to the Bulk method which is just "one".
But if you need the returned document ( option is the default in the driver anyway ) then either do that or similarly await the Promise resolve from the .bulkWrite() and then fetch the document via .findOne() after completion. Albeit that doing it via .findOne() after the modification would not truly be "atomic" and could possibly return the document "after" another similar modification was made, and not only in the state of that particular change.
N.B Also assuming that apart from the keys of the subdocuments in "pets" as a "set" that your other intention for the array contained was adding to that "set" as well via the additional content supplied to the function. If you just wanted to overwrite a value, then just apply $set instead of $addToSet and similarly wrap as an array.
But it sounds reasonable that the former was what you were asking.
BTW. Please clean up by horrible setup code in this example for the query and update objects in your actual code :)
As a self contained listing to demonstrate:
var async = require('async'),
mongodb = require('mongodb'),
MongoClient = mongodb.MongoClient;
MongoClient.connect('mongodb://localhost/test',function(err,db) {
var coll = db.collection('pettest');
var petName = "fido",
content = "bbb";
var filter1 = { "_id": 1 },
filter2 = { "_id": 1 },
update1 = { "$addToSet": {} },
update2 = { "$push": { "pets": {} } };
filter1["pets." + petName] = { "$exists": true };
filter2["pets." + petName] = { "$exists": false };
var setter1 = {};
setter1["pets.$." + petName] = content;
update1["$addToSet"] = setter1;
var setter2 = {};
setter2[petName] = [content];
update2["$push"]["pets"] = setter2;
console.log(JSON.stringify(update1,undefined,2));
console.log(JSON.stringify(update2,undefined,2));
function CleanInsert(callback) {
async.series(
[
// Clean data
function(callback) {
coll.deleteMany({},callback);
},
// Insert sample
function(callback) {
coll.insert({ "_id": 1, "pets": [{ "fido": ["abc"] }] },callback);
}
],
callback
);
}
async.series(
[
CleanInsert,
// Modify Bulk
function(callback) {
coll.bulkWrite([
{ "updateOne": {
"filter": filter1,
"update": update1
}},
{ "updateOne": {
"filter": filter2,
"update": update2
}}
]).then(function(res) {
console.log(JSON.stringify(res,undefined,2));
coll.findOne({ "_id": 1 }).then(function(res) {
console.log(JSON.stringify(res,undefined,2));
callback();
});
},callback);
},
CleanInsert,
// Modify Promise all
function(callback) {
var operations = [
coll.findOneAndUpdate(filter1,update1,{ "returnOriginal": false }),
coll.findOneAndUpdate(filter2,update2,{ "returnOriginal": false })
];
Promise.all(operations).then(function(res) {
//console.log(JSON.stringify(res,undefined,2));
console.log(
JSON.stringify(
res.filter(function(el) { return el.value != null })[0].value
)
);
callback();
},callback);
}
],
function(err) {
if (err) throw err;
db.close();
}
);
});
And the output:
{
"$addToSet": {
"pets.$.fido": "bbb"
}
}
{
"$push": {
"pets": {
"fido": [
"bbb"
]
}
}
}
{
"ok": 1,
"writeErrors": [],
"writeConcernErrors": [],
"insertedIds": [],
"nInserted": 0,
"nUpserted": 0,
"nMatched": 1,
"nModified": 1,
"nRemoved": 0,
"upserted": []
}
{
"_id": 1,
"pets": [
{
"fido": [
"abc",
"bbb"
]
}
]
}
{"_id":1,"pets":[{"fido":["abc","bbb"]}]}
Feel free to change to different values to see how different "sets" are applied.
Please try this one with string template, here is one example running under mongo shell
> var name = 'fido';
> var t = `pets.${name}`; \\ string temple, could parse name variable
> db.pets.find()
{ "_id" : ObjectId("56d7b5019ed174b9eae2b9c5"), "pets" : [ { "fido" : [ "abc" ]} ] }
With the following update command, it will not update it if the same pet name exists.
> db.pets.update({[t]: {$exists: false}}, {$addToSet: {pets: {[name]: []}}})
WriteResult({ "nMatched" : 0, "nUpserted" : 0, "nModified" : 0 })
If the pets document is
> db.pets.find()
{ "_id" : ObjectId("56d7b7149ed174b9eae2b9c6"), "pets" : [ { "fi" : [ "abc" ] } ] }
After update with
> db.pets.update({[t]: {$exists: false}}, {$addToSet: {pets: {[name]: []}}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
The result shows add the pet name if it does Not exist.
> db.pets.find()
{ "_id" : ObjectId("56d7b7149ed174b9eae2b9c6"), "pets" : [ { "fi" : [ "abc" ] }, { "fido" : [ ] } ] }

Convert flat data into hierarchy in mongodb using Mongoose

I have created a model which has following properties:
id
children
parent
If the parent is set to null the document will be treated as top level parent. Each document may or may not have list of children which just contains the id's of other document.
db.categories.insert( { _id: "MongoDB", children: [],name: '', parent: 'Databases'} )
db.categories.insert( { _id: "dbm", children: [], parent: 'Databases' } )
db.categories.insert( { _id: "Databases", children: [ "MongoDB", "dbm" ], parent: 'Programming' } )
db.categories.insert( { _id: "Languages", children: [], parent: 'Programming' } )
db.categories.insert( { _id: "Programming", children: [ "Databases", "Languages" ],parent: 'books' } )
db.categories.insert( { _id: "Books", children: [ "Programming" ],parent: null } )
from the above model, I need to generate an output like this:
{
name: "books",
nodes: [
{
name: "Programming",
nodes: [
{
name: "Databases",
nodes: [
{
name: "MonogoDB"
},
{
name: "dbm"
}
]
},
{
name: "Languages"
}
]
}
]
}
So far I have tried this, but because of async nature of findByID(), I am not getting the desired result.Please suggest changes.
myDoc.find({
parent: null
}, function (err, topics) {
if (err) {
return handleError(res, err);
}
var docTree = [];
_.each(document, function(parentDoc) {
var doc = {};
doc.text = parentDoc.name;
doc.nodes = [];
_.each(parentDoc.children, function(child) {
doc.nodes.push(processChildren(child));
});
docTree.push(doc);
});
function processChildren(child) {
myDoc.findById(child, function (err,item) {
var doc = {};
doc.name = item._doc.name;
doc.nodes = [];
if (item._doc.children === null) {
return topic;
} else {
_.each(item._doc.children, function (chld) {
processChildren(chld);
});
}
return doc;
});
};
You can try calling the lean() method after the find() method since documents returned from queries with the lean option enabled are plain JavaScript objects. You can then manipulate those objects to get the hierarchical structure you want. The following may not give the exact desired result but will push you into the right direction:
Model.find({}).lean().exec(function (err, docs) {
if (err) {
return handleError(res, err);
}
var i, len, temp, top_level, id, parent, hierarchical, nodePendingObj, doc, _id, _parent, _children;
i = 0;
top_level = [];
temp = {};
obj = {};
nodePendingObj = {};
_id = '_id';
_parent = 'parent';
_children = 'children';
_.each(docs, function(doc) {
id = doc['_id'];
parent = doc[_parent];
temp[id] = doc;
if (parent === undefined || parent === null) {
obj["name"] = id;
obj["nodes"] = doc[_children];
top_level.push(obj);
}
else {
if (temp[parent] !== undefined) {
if (temp[parent][_children] === undefined) {
temp[parent][_children] = [];
}
var o = {};
o["name"] = id;
if (doc[_children].length !== 0) o["nodes"] = doc[_children];
temp[parent][_children].push(o);
} else {
if (nodePendingObj[parent] === undefined) {
nodePendingObj[parent] = [];
}
var o = {};
o["name"] = id;
if (doc[_children].length !== 0) o["nodes"] = doc[_children];
nodePendingObj[parent].push(o);
}
delete doc[_parent];
}
if (nodePendingObj[id] !== undefined) {
var len = nodePendingObj[id].length;
if (doc[_children] === undefined) {
doc[_children] = [];
}
while (len-- > 0) {
doc[_children].push(nodePendingObj[id].shift());
}
}
});
if (top_level.length === 1) {
hierarchical = top_level[0];
} else if (top_level.length > 1) {
hierarchical = {};
hierarchical[_children] = top_level;
} else {
hierarchical = {};
}
console.log(JSON.stringify(hierarchical));
});
Check the demo below.
var documents = [
{
"_id" : "MongoDB",
"children" : [],
"name" : "",
"parent" : "Databases"
},{
"_id" : "dbm",
"children" : [],
"parent" : "Databases"
},{
"_id" : "Databases",
"children" : [
"MongoDB",
"dbm"
],
"parent" : "Programming"
},{
"_id" : "Languages",
"children" : [],
"parent" : "Programming"
},{
"_id" : "Programming",
"children" : [
"Databases",
"Languages"
],
"parent" : "books"
},{
"_id" : "books",
"children" : [
"Programming"
],
"parent" : null
}
];
var i, len, temp, top_level, id, parent, hierarchical, nodePendingObj, doc, _id, _parent, _children;
i = 0;
top_level = [];
temp = {};
obj = {};
nodePendingObj = {};
_id = '_id';
_parent = 'parent';
_children = 'children';
_.each(documents, function(doc) {
id = doc['_id'];
parent = doc[_parent];
temp[id] = doc;
if (parent === undefined || parent === null) {
obj["name"] = id;
obj["nodes"] = doc[_children];
top_level.push(obj);
}
else {
if (temp[parent] !== undefined) {
if (temp[parent][_children] === undefined) {
temp[parent][_children] = [];
}
var o = {};
o["name"] = id;
if (doc[_children].length !== 0) o["nodes"] = doc[_children];
temp[parent][_children].push(o);
} else {
if (nodePendingObj[parent] === undefined) {
nodePendingObj[parent] = [];
}
var o = {};
o["name"] = id;
if (doc[_children].length !== 0) o["nodes"] = doc[_children];
nodePendingObj[parent].push(o);
}
delete doc[_parent];
}
if (nodePendingObj[id] !== undefined) {
var len = nodePendingObj[id].length;
if (doc[_children] === undefined) {
doc[_children] = [];
}
while (len-- > 0) {
doc[_children].push(nodePendingObj[id].shift());
}
}
});
if (top_level.length === 1) {
hierarchical = top_level[0];
} else if (top_level.length > 1) {
hierarchical = {};
hierarchical[_children] = top_level;
} else {
hierarchical = {};
}
pre.innerHTML = JSON.stringify(hierarchical, null, 4);
<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js"></script>
<pre id="pre"></pre>

Resources