MongoDB : Nested array update issue - node.js

I am using MEAN stack to display the following array in a grid.
Nested array:
{
"_id":"1",
"appDomain":[
{
"appDomainName":"XYZ",
"dashboard":[
{
"envName":"UAT",
"envDetails":[
{
"hostnme":"ABC",
"ip":"sdsdsdsd",
"cpu":"-------",
"memory":"-------",
"disk":"-------",
"downtime":"sdsdsdsd",
"version":"dsdsdsd",
"hostDetails":[
{
"hostHdrName":"tomcat",
"hostDetails":[
{
"hostname":"FLT",
"status":"UP",
"path":"dfdf",
"host":"sdsdsd",
"port":"1112"
}
]
}
]
}
]
}
]
},
{
"appDomainName":"ABC",
"dashboard":[
{
"envName":"UAT",
"envDetails":[
{
"hostnme":"ABC",
"ip":"sdsdsdsd",
"cpu":"-------",
"memory":"-------",
"disk":"-------",
"downtime":"sdsdsdsd",
"version":"dsdsdsd",
"hostDetails":[
{
"hostHdrName":"tomcat",
"hostDetails":[
{
"hostname":"FLT",
"status":"UP",
"path":"dfdf",
"host":"dfdfdf",
"port":"1112"
},
{
"hostname":"SHP",
"status":"DOWN",
"path":"dfdfdf",
"host":"fgfgfg",
"port":"1112"
}
]
}
]
}
]
}
]
}
]
}
Mangoose update: I am using express.js to add/update and delete the entry in MangoDB. I am facing the issue in update in nested array. Please see below the code for update the nested array.
router.post('/appDomain/update/:appDomainName/:envName', function(req, res, next) {
AppDomain.findOne({}, {_id: 0, appDomain: {$elemMatch: {appDomainName: req.params.appDomainName,}}}, function (err, appDomain) {
if(appDomain.appDomain[0].dashboard[0].envName == req.params.envName )
appDomain.appDomain[0].dashboard[0].envDetails.push({})
appDomain.save(function (err) {
if(err) {
console.error('ERROR!');
}
});
res.json(appDomain);
});
})
However, it is not updating. It would be great if anyone can help me out..
AppDomainModel Schema
var Schema = mongoose.Schema;
var serverDetails = new Schema({
hostname: String,
ip: String,
status: String,
path: String,
host: String,
port: String
});
var hostDetail = new Schema({
hostHdrName: String,
hostDetails: [serverDetails]
});
var keyValue = new Schema({
keyHdrName: String,
keyValueData: [{key: String, value:String}]
});
var envSchema = new Schema({
hostnme: String,
ip: String,
cpu: String,
memory: String,
disk: String,
downtime: String,
version: String,
hostDetails: [hostDetail],
keyValues: [keyValue],
activities:{
recent: [keyValue],
planned: [keyValue]
}
});
var dashboardSchema = new Schema({
envName: String,
envDetails: [envSchema]
});
var appDomainSchema = new Schema({
_id:String,
appDomain:[{
appDomainName: {type:String,index: { unique: true }},
dashboard: [dashboardSchema]
}]
});
var AppDomain = mongoose.model('AppDomain', appDomainSchema);
var Dashboard = mongoose.model('Dashboard', dashboardSchema);
var EnvSchema = mongoose.model('EnvSchema', envSchema);
After updating.I am using the following function to check the value .. However the updated one is not available in the DB.
router.get('/app/domain/get/:appDomainName', function(req, res, next) {
AppDomain.find({}, {_id: 0, appDomain: {$elemMatch: {appDomainName: req.params.appDomainName,}}},function (err, appDomain) {
res.json(appDomain);
});
});

After a long struggle, I figured it out . See below the answer.
var data2 = {};
AppDomain.update(
{_id:1,'appDomain.appDomainName': req.params.appDomainName,'dashboard.$.envName':{$nin:[req.params.envName]}},
{$push: {'appDomain.$.dashboard.0.envDetails':{'envDetails':[data2]}}},
function(err, model) {
console.log(err);
}
);

I suspect your changes are actually being saved successfully, but you're returning the response too soon. Try this:
appDomain.save(function (err, savedAppDomain) {
if(err) {
console.error('ERROR!');
next(err);
return;
}
res.json(savedAppDomain):
});
// Removed:
//res.json(appDomain);
//});

Related

How to update subdocument value through mongoose?

I am trying to update the value of a nested document through a PUT request. It is working for values in the document, but not in the subdocument.
const AnotherSchema = new Schema ({
Name: String,
Age: Number,
Appearance: {
Hair: String, Eyes: String, Height: Number};
My route looks like this
router.put("/looks/:id/edit", function(req, res) {
var Name= "blob";
var Hair= "blue";
AnotherSchema.findByIdAndUpdate(req.params.id, {Name, Hair}, function(err, feedback){
if (err){
res.send("error");
} else {
res.redirect("/looks");
}
});
});
This route works for updating Name, but not Hair. I have tried Appearance.Hair, but this throws an error in the console for an unexpected . I have also tried [], () and {} but none of these do the trick, nor do " " and this issue does not seem to appear in the docs.
You should be providing the paths via the object notation to the props you want to update:
router.put("/looks/:id/edit", function(req, res) {
AnotherSchema.findByIdAndUpdate(req.params.id, {
Name: "blob",
Appearance: {
Hair: "blue"
}
}, function(err, feedback) {
if (err) {
res.send("error");
} else {
res.redirect("/looks");
}
});
});
The above findByIdAndUpdate is equivalent to:
{ $set: { Name: "blob", Appearance: { Hair: "blue" } } } as per the docs
You should use the $set operator, otherwise you will replace the whole record with the object provided as argument.
var updateObj = {
{ $set: { Name: "blob", Appearance: { Hair: "blue" } } }
};
AnotherSchema.findByIdAndUpdate(req.params.id, updateObj, function (err, feedback) { ... });

Collection not saved mongodb nodejs

I have an issue on my application (mongodb/nodejs), my aim is to save a collection :
const Job = require("../models/Job");
exports.saveJob = (req, res, next) => {
const newJob = new Job(req.body);
newJob.gender = 'Male';
newJob.save((err, myjob) => {
myjob.code = '1234';
myjob.save((err, mysavedjob) => {
console.log(mysavedjob);
/** OUTPUT **
* { __v: 0,
updatedAt: 2018-07-31T08:31:47.664Z,
createdAt: 2018-07-31T08:31:47.664Z,
gender:'Male',
code:'1234',
...
}
*/
})
})
}
As you can see, the code output the saved document, but when I check the database , I don't have the code:'1234' I have just
{ __v: 0,
updatedAt: 2018-07-31T08:31:47.664Z,
createdAt: 2018-07-31T08:31:47.664Z,
gender:'Male',
...
}
==== UPDATE (adding the JobModel) ====
const mongoose = require("mongoose");
const JobSchema = new mongoose.Schema(
{
userId: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
code: String,
gender: String,
},
{ timestamps: true }
);
module.exports = mongoose.model("Job", JobSchema, "jobs");
Someone could tell me why please ?
thank you.
I am not sure this is the correct way to alter a job after saving it but try this
const Job = require("../models/Job");
exports.saveJob = (req, res, next) => {
const newJob = new Job(req.body);
newJob.gender = 'Male';
newJob.save((err, myjob) => {
myjob.code = '1234';
Job.findOneAndUpdate({_id: myjob._id}, myjob, (err, mysavedjob) => {
console.log(mysavedjob);
});
})
}

Routing to sub docs with express 4 and mongoose

EDIT: It's possible the problem is an issue with pathing. my current query looks like this:
router.route('/projects/:project_id/techDetails')
.get(function(req, res) {
Project.findById(req.params.project_Id, function(err, project) {
if (err)
return res.send(err);
res.json(project);
console.log('get success (project techDetails)');
});
});
this returns null. even though it's identical to a working line of code in every way except for the addition of `/techDetails' to the route.
original question:
I'm building a MEAN stack app with express and mongo. I can't figure out how to route to nested documents properly.
here is my Project schema:
const ProjectSchema = new Schema({
idnumber: { type: Number, required: true },
customername: String,
projectdetails: String,
jobaddress: String,
techDetails: [{
scope: String,
edgedetail: String,
lamination: String,
stonecolour: String,
slabnumber: String,
slabsupplier: String,
purchaseordernum: String,
splashbacks: String,
apron: String,
hotplate: String,
sink: String,
sinkdetails: String,
tappos: String
}],
sitecontactname: String,
sitecontactnum: String,
specialreq: String,
install_date: String,
created_on: { type: Date, default: Date.now },
created_by: { type: String, default: 'SYSTEM' },
active: { type: Boolean, default: true },
flagged: { type: Boolean, default: false },
});
I can successfully route to /projects with GET and POST, and /projects/:project_id with GET, PUT and DEL.
using the PUT route and a project's _ID i can push new entries to a project's techDetails subdoc array. the resulting JSON data looks like this:
{
"_id": "59e577e011a3f512b482ef13",
"idnumber": 52,
"install_date": "10/20/2017",
"specialreq": "some...",
"sitecontactnum": "987654321",
"sitecontactname": "bill",
"jobaddress": "123 st",
"projectdetails": "some stuff",
"customername": "B Builders",
"__v": 16,
"flagged": false,
"active": true,
"created_by": "SYSTEM",
"created_on": "2017-10-17T03:24:16.423Z",
"techDetails": [
{
"scope": "Howitzer",
"edgedetail": "12mm",
"lamination": "No",
"stonecolour": "Urban™",
"slabnumber": "1",
"slabsupplier": "Caesarstone",
"purchaseordernum": "no",
"splashbacks": "No",
"apron": "No",
"hotplate": "N/A",
"sink": "N/A",
"sinkdetails": "no",
"tappos": "no",
"_id": "59e577e011a3f512b482ef14"
},
{
"scope": "kitchen",
"edgedetail": "12mm",
"lamination": "etc",
"_id": "59e7da445d9d7e109c18f38b"
},
{
"scope": "Vanity",
"edgedetail": "12mm",
"lamination": "No",
"stonecolour": "Linen™",
"slabnumber": "1",
"slabsupplier": "Caesarstone",
"purchaseordernum": "1",
"splashbacks": "No",
"apron": "No",
"hotplate": "N/A",
"sink": "N/A",
"sinkdetails": "no",
"tappos": "woo",
"_id": "59e81e3324fb750fb46f8248"
}//, more entries omitted for brevity
]
}
as you can see everything so far is working as expected. However now i need to edit and delete individual entries in this techDetails array. i'd also like to route to them directly using projects/:project_id/techDetails and projects/:project_id/techDetails/:techdetails_id.
From what i can see there are two approaches to this. either i can:
A) use a new routing file for the techDetails that uses mergeParams. this is the approach i'm trying currently, however I can't figure out how to complete the .find to return all techDetails, since i can only use the Project model schema and i'm unsure how to access the sub docs.
an excerpt from my routes.js:
const techDetails = require('./techDetails');
//other routes here
//see techdetails file
router.use('/projects/:project_id/techdetails', techDetails);
//here lies an earlier, failed attempt
/* router.route('/projects/:project_id/techdetails/:techDetails_id')
.get(function(req, res) {
Project.findById(req.params.project_id.techDetails_id, function(err,
project) {
if (err)
return res.send(err);
res.json(project.techDetails);
console.log('get success (techDetails)');
});
})
; */
and my techdetails.js:
const express = require('express');
const Project = require('./models/project');
const router = express.Router({mergeParams: true});
router.get('/', function (req, res, next) {
/* Project.find(function(err, techDetails) {
if (err)
return res.send(err);
res.json(techDetails);
console.log('get success (all items)');
}); */
res.send('itemroutes ' + req.params);
})
router.get('/:techDetails_id', function (req, res, next) {
res.send('itemroutes ' + req.params._id)
})
module.exports = router
I can successfully check that the routes work with Postman, both will receive the response. now the problem is, instead of res.send i want to use res.json with Project.find (or similar) to get the techDetails.
however there is also another option:
B) put the techDetails document into it's own schema and then populate an array of IDs inside projects.
however this seems more complex so i'd rather avoid having to do so if i can.
any thoughts and suggestions welcome. let me know if more of my code is needed.
In this particular case I would put techDetails in a separate schema:
const ProjectSchema = new Schema({
idnumber: { type: Number, required: true },
customername: String,
projectdetails: String,
jobaddress: String,
techDetails: [techDetailsSchema],
sitecontactname: String,
sitecontactnum: String,
specialreq: String,
install_date: String,
created_on: { type: Date, default: Date.now },
created_by: { type: String, default: 'SYSTEM' },
active: { type: Boolean, default: true },
flagged: { type: Boolean, default: false },
});
Don't register the techDetails schema with mongoose.model as it is a subdocument. Put it in a separate file and require it in the project model file (const techDetailsSchema = require('./techDetails.model');).
I would create the controller functions like this:
Getting with GET (all):
module.exports.techDetailsGetAll = function (req, res) {
const projectId = req.params.projectId;
Project
.findById(projectId)
.select('techDetails')
.exec(function (err, project) {
let response = { };
if (err) {
response = responseDueToError(err);
} else if (!project) {
response = responseDueToNotFound();
} else {
response.status = HttpStatus.OK;
response.message = project.techDetails;
}
res.status(response.status).json(response.message);
});
}
Getting with GET (one):
module.exports.techDetailsGetOne = function (req, res) {
const projectId = req.params.projectId;
const techDetailId = req.params.techDetailId;
Project
.findById(projectId)
.select('techDetails')
.exec(function (err, project) {
let response = { };
if (err) {
response = responseDueToError(err);
} else if (!project) {
response = responseDueToNotFound();
} else {
let techDetails = project.techDetails.id(techDetailId);
if (techDetails === null) {
response = responseDueToNotFound();
} else {
response.status = HttpStatus.OK;
response.message = techDetails;
}
}
res.status(response.status).json(response.message);
});
}
For adding with POST:
module.exports.techDetailsAddOne = function (req, res) {
const projectId = req.params.projectId;
let newTechDetails = getTechDetailsFromBody(req.body);
Project
.findByIdAndUpdate(projectId,
{ '$push': { 'techDetails': newTechDetails } },
{
'new': true,
'runValidators': true
},
function (err, project) {
let response = { };
if (err) {
response = responseDueToError(err);
} else if (!project) {
response = responseDueToNotFound();
} else {
response.status = HttpStatus.CREATED;
response.message = project.techDetails; // for example
}
res.status(response.status).json(response.message);
});
}
For updating with PUT
module.exports.techDetailsUpdateOne = function (req, res) {
const projectId = req.params.projectId;
const techDetailId = req.params.techDetailId;
let theseTechDetails = getTechDetailsFromBody(req.body);
theseTechDetails._id = techDetailId; // can be skipped if body contains id
Project.findOneAndUpdate(
{ '_id': projectId, 'techDetails._id': techDetailId },
{ '$set': { 'techDetails.$': theseTechDetails } },
{
'new': true,
'runValidators': true
},
function (err, project) {
let response = { };
if (err) {
response = responseDueToError(err);
res.status(response.status).json(response.message);
} else if (!project) {
response = responseDueToNotFound();
res.status(response.status).json(response.message);
} else {
project.save(function (err) {
if (err) {
response = responseDueToError(err);
} else {
response.status = HttpStatus.NO_CONTENT;
}
res.status(response.status).json(response.message);
})
}
});
}
And deleting with DELETE:
module.exports.techDetailsDeleteOne = function (req, res) {
const projectId = req.params.projectId;
const techDetailId = req.params.techDetailId;
Project
.findById(projectId)
.select('techDetails')
.exec(function (err, project) {
let response = { }
if (err) {
response = responseDueToError(err);
res.status(response.status).json(response.message);
} else if (!project) {
response = responseDueToNotFound();
res.status(response.status).json(response.message);
} else {
let techDetail = project.techDetails.id(techDetailId);
if (techDetail !== null) {
project.techDetails.pull({ '_id': techDetailId });
project.save(function (err) {
if (err) {
response = responseDueToError(err);
} else {
response.status = HttpStatus.NO_CONTENT;
}
res.status(response.status).json(response.message);
})
} else {
response = responseDueToNotFound();
res.status(response.status).json(response.message);
}
}
});
}
And finally routing like this:
router
.route('/projects')
.get(ctrlProjects.projectsGetAll)
.post(ctrlProjects.projectsAddOne);
router
.route('/projects/:projectId')
.get(ctrlProjects.projectsGetOne)
.put(ctrlProjects.projectsUpdateOne)
.delete(ctrlProjects.projectsDeleteOne);
router
.route('/projects/:projectId/techDetails')
.get(ctrlTechDetails.techDetailsGetAll)
.post(ctrlTechDetails.techDetailsAddOne);
router
.route('/projects/:projectId/techDetails/:techDetailId')
.get(ctrlTechDetails.techDetailsGetOne)
.put(ctrlTechDetails.techDetailsUpdateOne)
.delete(ctrlTechDetails.techDetailsDeleteOne);
This is what I prefer when I'm constantly updating the subdocument independently of the rest of the document. It doesn't create a separate collection, so no need for populate.
EDIT:
This answer goes more into detail on whether you should use embedding or referencing. My answer uses embedding.
So, the solution i came to was a combo of A) and B). I used a separate routing file and put ({mergeParams: true}) in the router declaration, and i created a separate file for the techDetails nested model, without declaring it. However I don't believe either of these actually made any significance... but anyway.
the working code i ended up with was, in my routes:
router.use('/projects/:project_id/techDetails', TechDetails);
and in techDetails.js:
const router = express.Router({mergeParams: true});
router.route('/')
.get(function(req, res) {
Project.findById(req.params.project_id,
'techDetails', function(err, project) {
if (err)
return res.send(err);
res.json(project);
console.log('get success (project techDetails)');
});
});
What's different about it? namely, the 'techDetails', parameter in the Project.findById line. According to the mongoose API this acts as a select statement. The only other major difference is I fixed a typo in my original code ( project_id was written project_Id. dubious... ). I probably would have noticed this if i was using VS or something instead of notepad++, but it is my preferred coding arena.
It may be possible to return res.json(project.techDetails) and remove the 'techDetails', select parameter, but I likely won't test this.
Edit: Turns out migrating techDetails to a separate file meant they no longer generated with objectIds, which is crucial for PUT and DEL. I might've been able to work around them with a simple pair of curly braces inside the array declaration, but I didn't think of that until after i re-migrated it back to the project schema...

Populate with inherited document in Mongoose

I am trying to create a database schema for the following model:
I am not sure what the better way to represent this in a MongoDb would be, but since I am using Mongoose and there is a plugin for inheritance, I am trying the following:
var mongoose = require('mongoose')
, extend = require('mongoose-schema-extend')
, Schema = mongoose.Schema
, ObjectId = mongoose.Schema.Types.ObjectId
var db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', function callback () {
//Mashup and Item are bound (itemSchema is a sub-doc)
var itemSchema = new Schema({
pos: { top: Number, left: Number }
, size: { width: Number, height: Number }
, component: { type: ObjectId, ref: 'Component' }
})
var mashupSchema = new Schema({
name: String
, desc: String
, size: { width: Number, height: Number }
, items: [itemSchema]
})
var componentSchema = new Schema({
name: String
, desc: String
}, { discriminatorKey : '_type' })
var imageComponentSchema = componentSchema.extend({
url: String
})
var textComponentSchema = componentSchema.extend({
text: String
})
var htmlComponentSchema = componentSchema.extend({
html: String
})
var webComponentSchema = componentSchema.extend({
page: { type: ObjectId, ref: 'Page' }
, selector: { type: ObjectId, ref: 'Selector' }
})
var pageSchema = new Schema({
name: String
, desc: String
, url: String
, active: { type: Boolean, default: false }
, webComponents: [{ type: ObjectId, ref: 'WebComponent' }]
})
var selectorSchema = new Schema({
desc: String
, url: String
, cssPath: String
})
///MODELS
var Mashup = db.model("Mashup", mashupSchema)
var Component = db.model("Component", componentSchema)
var ImageComponent = db.model("ImageComponent", imageComponentSchema)
var TextComponent = db.model("TextComponent", textComponentSchema)
var HtmlComponent = db.model("HtmlComponent", htmlComponentSchema)
var WebComponent = db.model("WebComponent", webComponentSchema)
var Page = db.model("Page", pageSchema)
var Selector = db.model("Selector", selectorSchema)
//CREATE
//a new empty mashup
//var aMashup = new Mashup({ name: "Test" });
Mashup.create({ name: "Test" }, function (err, mashup) {
if (err) return
console.log("Saved: empty mashup")
//mashup saved, create a webComponent
var aWebComponent = new WebComponent({ name: "Map", desc: "A map" })
//create a page
var aPage = new Page({ name: "Maps", desc: "Google Maps", url: "http://maps.google.com" })
aPage.webComponents.push(aWebComponent)
aWebComponent.page = aPage
//create a selector
var aSelector = new Selector({desc: "Just the map", url: "maps.google.com", cssPath: "#map" })
aWebComponent.selector = aSelector
//save the component
aWebComponent.save(function(err) {
if (err) return
console.log("Saved: WebComponent")
aPage.save(function(err) {
if (err) return
console.log("Saved: the Page")
aSelector.save(function(err) {
if (err) return
console.log("Saved: the Selector")
//finally add the item with the new component
var item = { pos: { top:6, left:10 }, size: { width:100, height:100}, component: aWebComponent }
mashup.items.push(item)
mashup.save(function (err) {
if (err) return
console.log("Saved: mashup with item (WebComponent with Page and Selector)")
//POPULATE
Mashup
.find({})
.populate("items.component")
.exec(function (err, mashup) {
if (err) console.log(err)
console.log(mashup);
})
})
})
})
})
})
});
This is a use case scenario, where a user creates a Mashup and then adds a new Item to it by creating a new WebComponent. I need that Item class because each different mashup should be able to have "instances" (i.e. the Items) of existing Components.
Now, I am new to Mongoose and I am sure things could be done differently. Any suggestion here is welcome. However, when I try to query the Mashups populating the results, the output I get is:
Saved: empty mashup
Saved: WebComponent
Saved: the Page
Saved: the Selector
Saved: mashup with item (WebComponent with Page and Selector)
[ { __v: 1,
_id: 520a8aae3c1052f723000002,
name: 'Test',
items:
[ { component: null,
_id: 520a8aaf3c1052f723000006,
size: [Object],
pos: [Object] } ],
size: {} } ]
component should be populated but it is not. I guess this is because it expects a Componentwhile it gets a WebComponent. How do I fix this? Should I stop trying with inheritance? What other ways are there to create a DB schema for this model?
Doh.. changing
var componentSchema = new Schema({
name: String
, desc: String
}, { discriminatorKey : '_type' })
to
var componentSchema = new Schema({
name: String
, desc: String
}, { collection : 'components', discriminatorKey : '_type' })
Fixes the issue. Not sure why.

Mongoose - create reference on model save

In mongoose, is it possible to create a referenced document while saving the document it is being referenced in? I have tried the below but it does not seem to work for me.
var Model1Schema = new Schema({
foo: String,
child: { ref: 'Model2', type: ObjectId }
});
var Model2Schema = new Schema({
foo: String
});
mongoose.model('Model1', Model1Schema);
mongoose.model('Model2', Model2Schema);
var m = new (mongoose.model('Model1'));
m.set({
foo: 'abc',
child: {
bar: 'cba'
}
}).save();
Mongoose validation won't allow child to be created since it is a reference, so the second-best thing you can do is creating your own function to create an instance with the corrected child, that has already been saved. Something similar to this, I imagine..
var Model1Schema = new mongoose.Schema({
foo: String,
child: { ref: 'Model2', type: mongoose.Schema.ObjectId }
});
var Model2Schema = new mongoose.Schema({
foo: String
});
var Model1 = mongoose.model('Model1', Model1Schema);
var Model2 = mongoose.model('Model2', Model2Schema);
function CreateModel1WithStuff(data, cb) {
if (data.child) { // Save child model first
data.child = Model2(data.child);
data.child.save(function(err) {
cb(err, err ? null : Model1(data));
});
} else { // Proceed without dealing with child
cb(null, Model1(data));
}
}
CreateModel1WithStuff({
foo: 'abc',
child: {
bar: 'cba'
}
}, function(err, doc) {
doc.save();
});

Resources