Mongoose - how to set document property to be another document - node.js

I am trying to fake non-array nested documents by creating a separate model for the embedded document, validating it and if the validation is successful, setting it as the main document's property.
In a POST /api/document route I am doing teh following:
var document = new DocumentModel({
title: req.body.title
});
var author = new AuthorModel({
name: req.body.author.name
});
author.validate( function( err ) {
if (!err) {
document.author = author.toObject();
} else {
return res.send( err, 400 );
}
});
console.log( document );
But it doesn't seem to work - console prints out the document without author. I am probably missing something very obvious here, maybe I need to do some kind of nested callbacks, or maybe I need to use a special setter method, like document.set( 'author', author.toObject() )... but I just can't figure it our on my own right now.

Looks like author.validate is async so that your console.log(document); statement at the bottom executes before the callback where you set document.author. You need to put the processing that depends on document.authorbeing set inside of the callback.

It looks like the answer is to use a callback to set the document.author and to define author in the Schema.
Like #JohnnyHK pointed out, I can't log the document to console using my original code because the author.validate is async. So, the solution is to either wrap the console.log (and probably further document.save() inside the callback of author.validate()
It also seems that Mongoose does not "set" any properties for a model that are not defined in the Schema. Since my author is an object, I had to set the author field in the Schema to Mixed, like this.
The following code works:
var DocumentModel = new Schema({
title: { type: String, required: true },
author: {}
});
var AuthorModel = new Schema({
name: { type: String, required: true }
});
app.post("/api/documents", function(req, res) {
var document = new DocumentModel({
title: req.body.title
});
var author = new AuthorModek({
title: req.body.author.name
});
author.validate( function( err ) {
if (!err) {
document.author = author;
docment.save( function( err ) {
...
});
} else {
return res.send( err, 400 );
}
})
});

Related

How to save the result in variable of findOne() Mongoose using nodejs

I am trying to save the result of findOne(), however, I do not have any idea how to save this result in a variable.
function createSubscription (req, res, next) {
let product_id = "P-5JM98005MT260873LLT44E2Y" ;
let doc = null;
product.findOne({"plans.id": product_id}, { "plans.$": 1
}).sort({create_time: -1}).exec(function(err, docs) {
if(err) {
} else {
if(docs != null) {
this.doc = docs;
}
}
});
console.log(doc);
let result = null;
if (doc.create != null) {
result = processDoc(doc);
}
}
function processDoc(doc) {
//do something
return resul;
}
function processResult(result) {
//do something
}
Below, I copy the product schema
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const ProductSchema = new Schema({
id: {
type: String,
trim: true,
required: true,
},
name: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
create_time: {
type: Date,
required: true,
}
});
module.exports = mongoose.model('Product', ProductSchema);
The doc is always null and does not receive the value.
In general terms, I would like to get the response product.findOne to use another function, calling by createSubscription()
To save the result of findOne() is as easy as this:
var doc = product.findOne();
The problem you're having is that you are calling processDoc() before findOne() is finished. Look into asynchronous javascript. You can fix this by using async/await like this:
async function createSubscription (req, res, next) {
var doc = await product.findOne();
processDoc(doc);
}
The reason is you are calling a callback function, which means function is asynchronous. So there is no guarantee that your query will execute and the values will be set before it reaches to,
if (doc.create != null) {
result = processDoc(doc);
}
To resolve this you may use the Async/Await Syntax:
const createSubscription = async function (params) {
try { return await User.findOne(params)
} catch(err) { console.log(err) }
}
const doc = createSubscription({"plans.id": product_id})
Since you want to query something in database, that means you already created one and saved some data in it.
However before saving data in the database you should be creating your model which should be created under models folder in Product.js(model names should start capital letter in convention) . Then you have to IMPORT it in your above page to access it. You want to query by product.id but which product's id?
Since you have req in your function, that means you are posting something. If you set up the proper settings in app.js you should be able to access to req.body which is the information that being posted by the client side.
app.js //setting for parsing(reading or accessing) req.body
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
Now we can reach req.body.id
this should be your code:
function createSubscription (req, res, next) {
const product=Product.findOne({plan.id:req.body.id})
.
.
}
If you noticed I did not put this plan.id in quotes.Because findOne() method belongs the Product model and that model belongs to the package that you are using. (In mongoose you query without quotes, in mongodb client you query in quotes)
findOne() is an asynchronous operation means result will not come to you immediately. So you should always keep it in try/catch block.
function createSubscription (req, res, next) {
try{
const product=Product.findOne({plan.id:req.body.id})
}
catch(error){console.log(error.message)} //every error object has message property
.
.
}
Lastly since you are querying only one object you do not need to sort it.

Mongoose can't search by number field

I have a schema that has an id field that is set to a string. When I use collection.find({id: somenumber}) it returns nothing.
I've tried casting somenumber to a string and to a number. I've tried sending somenumber through as a regex. I've tried putting id in quotes and bare... I have no idea what's going on. Any help and input would be appreciated.
Toys.js
var Schema = mongoose.Schema;
var toySchema = new Schema( {
id: {type: String, required: true, unique: true},
name: {type: String, required: true},
price: Number
} );
My index.js is as such
app.use('/findToy', (req, res) => {
let query = {};
if (req.query.id)
query.id = req.query.id;
console.log(query);
// I've tried using the query variable and explicitly stating the object as below. Neither works.
Toy.find({id: '123'}, (err, toy) => {
if (!err) {
console.log("i'm right here, no errors and nothing in the query");
res.json(toy);
}
else {
console.log(err);
res.json({})
}
})
I know that there is a Toy in my mongoDB instance with id: '123'. If I do Toy.find() it returns:
[{"_id":"5bb7d8e4a620efb05cb407d2","id":"123","name":"Dog chew toy","price":10.99},
{"_id":"5bb7d8f7a620efb05cb407d3","id":"456","name":"Dog pillow","price":25.99}]
I'm at a complete loss, really.
This is what you're looking for. Visit the link for references, but here's a little snippet.
For the sake of this example, let's have a static id, even though Mongo creates a dynamic one [ _id ]. Maybe that what is the problem here. If you already a record in your DB with that id, there's no need for adding it manually, especially not the already existing one. Anyways, Drop your DB collection, and try out this simple example:
// Search by ObjectId
const id = "123";
ToyModel.findById(id, (err, user) => {
if(err) {
// Handle your error here
} else {
// If that 'toy' was found do whatever you want with it :)
}
});
Also, a very similar API is findOne.
ToyModel.findOne({_id: id}, function (err, toy) { ... });

How to cancel a mongoose query in a 'pre' hook

I am implementing some kind of caching for my 'find' queries on a certain schemas, and my cache works with the pre\post query hooks.
The question is how can I cancel the 'find' query correctly?
mySchema.pre('find', function(next){
var result = cache.Get();
if(result){
//cancel query if we have a result from cache
abort();
} else {
next();
}
});
so that this promise will be fulfilled?
Model.find({..})
.select('...')
.then(function (result) {
//We can reach here and work with the cached results
});
I was unable to find a reasonable solution to this myself for another non-caching reason but if your own specific caching method isn't too important I'd recommend you look at mongoose-cache, works well and has simple settings thanks to it's dependency: node-lru-cache, check that out for more options.
you may want to check out mongoose validators, that seems like a better way to handle controlling whether or not an object gets created.
You can create a custom validate function that will throw an error in the Model.save function, causing it to fail. Here is a code snippet from the docs:
// make sure every value is equal to "something"
function validator (val) {
return val == 'something';
}
new Schema({ name: { type: String, validate: validator }});
// with a custom error message
var custom = [validator, 'Uh oh, {PATH} does not equal "something".']
new Schema({ name: { type: String, validate: custom }});
// adding many validators at a time
var many = [
{ validator: validator, msg: 'uh oh' }
, { validator: anotherValidator, msg: 'failed' }
]
new Schema({ name: { type: String, validate: many }});
// or utilizing SchemaType methods directly:
var schema = new Schema({ name: 'string' });
schema.path('name').validate(validator, 'validation of {PATH} failed with value {VALUE}');
Found that here if you want to look into it more. Hope that helps someone!
http://mongoosejs.com/docs/api.html#schematype_SchemaType-validate

Initializing a ref array in Mongoose

Say I have the following schemas:
var promoGroupSchema = new Schema({
title: String,
offers: [{Schema.Types.ObjectId, ref: 'Offer']
});
and
var offerSchema = new Schema({
type: String
});
How do you initialize a promoGroup with new offers? The following won't work since save() is asynchronous. Now, I know I could put a function as a parameter of the save function, but that gets ugly with more offers.
var offer1 = new offerSchema({
type: "free bananas!"
});
var offer2 = new offerSchema({
type: "free apples!"
});
offer1.save();
offer2.save();
var newPromoGroup = new promoGroupSchema({
title: "Some title here",
offers: [offer1._id, offer2._id]
});
From what I read, Mongoose gives the object an _id as soon as you create them, can I rely on those?
You should access _id in the save callback. If you have a lot of offers to group, using a library like async will make your life easier.
var myOffers = [...]; // An array with offers you want to group together
// Array of functions you want async to execute
var saves = myOffers.map(function(offer) {
return function(callback) {
offer.save(callback);
}
}
// Run maximum 5 save operations in parallel
async.parallelLimit(saves, 5, function(err, res) {
if(err) {
console.log('One of the saves produced an error:', err);
}
else {
console.log('All saves succeeded');
var newPromoGroup = new promoGroupSchema({
title: "Some title here",
offers: _.pluck(myOffers, '_id') // pluck: see underscore library
});
}
});
You could also try to use Promises.

Incorrect Subdocument Being Updated?

I've got a Schema with an array of subdocuments, I need to update just one of them. I do a findOne with the ID of the subdocument then cut down the response to just that subdocument at position 0 in the returned array.
No matter what I do, I can only get the first subdocument in the parent document to update, even when it should be the 2nd, 3rd, etc. Only the first gets updated no matter what. As far as I can tell it should be working, but I'm not a MongoDB or Mongoose expert, so I'm obviously wrong somewhere.
var template = req.params.template;
var page = req.params.page;
console.log('Template ID: ' + template);
db.Template.findOne({'pages._id': page}, {'pages.$': 1}, function (err, tmpl) {
console.log('Matched Template ID: ' + tmpl._id);
var pagePath = tmpl.pages[0].body;
if(req.body.file) {
tmpl.pages[0].background = req.body.filename;
tmpl.save(function (err, updTmpl) {
console.log(updTmpl);
if (err) console.log(err);
});
// db.Template.findOne(tmpl._id, function (err, tpl) {
// console.log('Additional Matched ID: ' + tmpl._id);
// console.log(tpl);
// tpl.pages[tmpl.pages[0].number].background = req.body.filename;
// tpl.save(function (err, updTmpl){
// if (err) console.log(err);
// });
// });
}
In the console, all of the ID's match up properly, and even when I return the updTmpl, it's saying that it's updated the proper record, even though its actually updated the first subdocument and not the one it's saying it has.
The schema just in case:
var envelopeSchema = new Schema({
background: String,
body: String
});
var pageSchema = new Schema({
background: String,
number: Number,
body: String
});
var templateSchema = new Schema({
name: { type: String, required: true, unique: true },
envelope: [envelopeSchema],
pagecount: Number,
pages: [pageSchema]
});
templateSchema.plugin(timestamps);
module.exports = mongoose.model("Template", templateSchema);
First, if you need req.body.file to be set in order for the update to execute I would recommend checking that before you run the query.
Also, is that a typo and req.body.file is supposed to be req.body.filename? I will assume it is for the example.
Additionally, and I have not done serious testing on this, but I believe your call will be more efficient if you specify your Template._id:
var template_id = req.params.template,
page_id = req.params.page;
if(req.body.filename){
db.Template.update({_id: template_id, 'pages._id': page_id},
{ $set: {'pages.$.background': req.body.filename} },
function(err, res){
if(err){
// err
} else {
// success
}
});
} else {
// return error / missing data
}
Mongoose doesn't understand documents returned with the positional projection operator. It always updates an array of subdocuments positionally, not by id. You may be interested in looking at the actual queries that mongoose is building - use mongoose.set('debug', true).
You'll have to either get the entire array, or build your own MongoDB query and go around mongoose. I would suggest the former; if pulling the entire array is going to cause performance issues, you're probably better off making each of the subdocuments a top-level document - documents that grow without bounds become problematic (at the very least because Mongo has a hard document size limit).
I'm not familiar with mongoose but the Mongo update query might be:
db.Template.update( { "pages._id": page }, { $set: { "pages.$.body" : body } } )

Resources