How to add object to nested array in mongoose? - node.js

In essence what I am trying to do is something along the lines of FindByIdAndCreate, a method which does not exist in mongoose.
I have a schema as so:
const WordSchema = new Schema ({
TargetWord: String,
Translation: String,
ExampleSentences: [{
Number: Number, //increment somehow each time
Sentence: String,
}],
});
I have a form where the user can add example sentences of this target word, the route for which looks like this:
router.put("/word/:id/add", async(req, res) => {
//get the new sentence from the field
var NewSentence = req.body.Sentence;
Now once I have this new sentence saved to the variable NewSentence I want to create a new object within the WordSchema.ExampleSentences array which contains the new sentences itself, and the number which should automatically increment.
I have fiddled around with FindByIdAndUpdate to no avail,this syntax does not work because it throws an error at the use of .
WordSchema.findByIdAndUpdate(req.params.id, {ExampleSentences.Sentence: NewSentence}, ...

The only solution to increment your counter is to retrieve every document with a good old 'find' and create your new entry accordingly, since 'update' has no way to self reference a document during the process.
router.put("/word/:id/add", async(req, res) => {
WordSchema.find({_id: req.body.id}, function(results) {
if (results.length === 0) return res.json();
const word = result[0];
const NewExampleSentence = {
Number: word.ExampleSentences.length, // if your counter start from 1, then add 1
Sentence: req.body.Sentence
};
word.ExampleSentences.push(NewExampleSentence);
word.save(function(err) {
if (err) {
// handle error
}
return res.json();
})
}
})

Related

Mongoose: After finding document, iterate over a value in the document and run a new query on each

I have one schema which contains an array of references to another schema (among other fields):
const RecipeIngredient = new Schema({
ingredientId: { // store id ref so I can populate later
type: Schema.Types.ObjectId,
ref: 'ingredients',
required: true
},
// there are a couple other fields but not relevant here
});
const Recipe = new Schema({
ingredients: [RecipeIngredient]
});
I'm trying to write a route which will first find a recipe by _id, populate the ingredients array (already have this working), and finally iterate over each ingredient in that array.
router.get('/:recipeId/testing', async (req, res) => {
const { recipeId } = req.params
let recipe = await Recipe
.findById(recipeId)
.populate({
path: 'ingredients.ingredientId',
model: 'Ingredient',
select: '_id ......' //I'm selecting other fields too
})
.lean()
.exec();
if (recipe) {
const { ingredients } = recipe;
const newIngredients = [];
await ingredients.forEach(async (ingr) => {
// here I'd like to be able to run a new query
// and append the result to an array outside of the forEach
// I do need information about the ingr in order to run the new query
newIngredients.push(resultOfNewQuery);
});
return res.json(newIngredients)
};
return res.status(404).json({ noRecipeFound: 'No recipe found.'});
})
I've tried approaching this in a few different ways, and the closest I've gotten was executing the new query within each iteration, but because the query is async, I return the response before I've actually collected the documents from the inner query.
I also attempted to use .cursor() in the initial query, but that won't work for me because I do need to access the ingredients field on the recipe once it is resolved before I can iterate and run the new queries.
Any ideas would be appreciated! I'm definitely opening to restructuring this whole route if my approach is not ideal.
I was able to make this work by using a for loop:
const newIngredients = [];
for (let idx = 0; idx < ingredients.length; idx++) {
const { fieldsImInterestedIn } = ingredients[idx];
const matchingIngredients = await Ingredient
.find(fieldsImInterestedIn)
.lean()
.exec()
.catch(err => res.status(404).json({ noIngredientsFound: 'No ingredients found' }));
newIngredients.push(ingredientsToChooseFrom[randomIndex]);
};
return res.json(newIngredients);
still a little perplexed as to why this was able to work while forEach wasn't, but I'll happily move on...

Modifying array directly in mongoose doesn´t works

I have a mongoose schema with a field array.
I need to set the array field directly without get rid off the existent values.
I am using
item.files.concat(myfiles);
in the next code, but it doesn´t work. Only the last item from array is saved
//code
var files=['file1','file2','file3']
var myfiles=[]
files.forEach(function(file){
myfiles.push({title:file});
}
});
//router
FileSchema.findById(id).exec( (err,item) => {
//fill files -- error is here
item.files.concat(myfiles);
item.save(function (err, data) {
if (err) {
return res.status(500).send(err)
}
res.send(data); //send response
})
})
//schema
const mongoose=require('mongoose');
const fileSchema = new mongoose.Schema({
id: {type: Number, unique:true},
...
...
files:[{title:{type: String}}]
});
From MDN web docs:
The concat() method is used to merge two or more arrays. This method does not change the existing arrays, but instead returns a new array.
item.files = item.files.concat(myfiles);

Result of mongoose.save is incorrect when adding item to mixed schema array

Following is a function that shows the issue:
var mongoose = require('mongoose');
var connection = mongoose.createConnection('mongodb://localhost:27017');
connection.once('open', function () {
var schema = new mongoose.Schema({
obj: [{}] //mongoose.Schema.Types.Mixed
});
var Model = connection.model('mtest', schema);
var model = new Model({
obj: [{ name: 'Original' }]
});
model.save(function (err, res) {
console.log('result 1', res);
Model.findOne({_id: res._id}, function (err, res) {
res.obj[0].name = 'Modified';
res.obj.push({ name: 'other' });
//res.markModified('obj'); // using markModified does not help
res.save(function (err, res) {
console.log('result 2', res);
connection.close();
process.exit();
});
});
})
});
The output of "result 2" shows "Modified" for the first item in "obj": obj: [ { name: 'Modified' }, { name: 'other' } ].
However, in the database the value of the first item is still "Original".
This only happens when pushing a second item into the array (otherwise the first item is indeed modified).
Adding markModified does resolve the issue.
I'm using an array of empty objects types in the schema because in reality this use case deals with with schemas that inherit from each other, so no single schema can be used here.
Is it a bug? The only workaround I've found is to clear the array and add all the items again. I'd like to know if there's a better solution.
You could either alter your markModified call to identify the index of the element you changed "outside" of the array access methods:
res.obj[0].name = 'Modified';
res.obj.push({ name: 'other' });
res.markModified('obj.0');
Or switch to using the set array access method to alert name (which looks pretty goofy, but does work):
res.obj[0].name = 'Modified';
res.obj.set(0, res.obj[0]);
res.obj.push({ name: 'other' });

How to remove mongo specific fields from result (NodeJS, Mongoose)

I want to remove all Mongo specific fields (like '_id') from query result. Is there a simple method to do this or should I remove fields manually? If yes, then which are that fields and how to do that?
I'm using NodeJS and Mongoose
You can use select() method for remove the field from your query:
Model.find({}).select("-removed_field").then (resp => {
// your code
});
You should specified the "-" before field name, to be remove this field.
If you want remove several fields - you can specified their as array:
Model.find({}).select(["-removed_field1", "-removed_field2" ... ]).then (resp => {
// your code
});
Also you can to select only specified fields, using this method without "-"
Model.find({}).select(["field1", "field2" ... ]).then (resp => {
// your code
});
If you want hide _id property you can use text argument with prefix - which will exclude this or that field from the result, for get sepecifict fields you should pass like this:
Entity.find({ ... }, 'field1 field2', function(err, entity) {
console.log(entity); // { field1: '...', field2: '...' }
});
You can specify a field to be excluded from results by using the optional 2nd parameter projection string of the find method:
Model.find({}, "-a -b").then (res => {
// objects in the res array will all have the
// 'a' and 'b' fields excluded.
});
https://mongoosejs.com/docs/api.html#model_Model.find (see projection)
you can use mongoose instance method two show specific fields from all documents
const userSchema = new mongoose.Schema({
email: {
type: String,
},
name: {
type: String,
maxlength: 128,
index: true,
trim: true,
},
});
userSchema.method({
transform() {
const transformed = {};
const fields = ['name', 'email'];
fields.forEach((field) => {
transformed[field] = this[field];
});
return transformed;
},
});
module.exports = mongoose.model('User', userSchema);
if You want to remove any specific fields like _id, You can try in two ways:
Suppose Here you try to find a user using User Model
User.find({ email: email }, { _id: 0 });
OR
const user = User.find({ email: email });
delete user._doc._id;
OP mentioned "from result", as far as I understood, it means, removing from the query result i.e. query result will contain the field, but will be removed from the query result.
A SO answer here mentions, that to modify a query result (which are immutable), we've to convert the result to Object using toObject() method (making it mutable).
To remove a field from a query result,
let immutableQueryResult = await Col.findById(idToBeSearched)
let mutableQueryResult = immutableQueryResult.toObject()
delete mutableQueryResult.fieldToBeRemoved
console.log(mutableQueryResult)
Another way of getting the mutable result is using the _doc property of the result:
let immutableQueryResult = await Col.findById(idToBeSearched)
let mutableQueryResult = immutableQueryResult._doc // _doc property holds the mutable object
delete mutableQueryResult.fieldToBeRemoved
console.log(mutableQueryResult)

Mongoose find unique tags

Currently saving an array of tags for each user. The issue I'm running into is how to search for unique tags (so I can do a type ahead for adding new users) Right now if I get a match, the function returns the WHOLE tags array that contain the tag fragment rather than just the individual tag of the user that matches.
How do I specify that I only want to return unique title values rather than the whole tag array?
var UserSchema = new Schema({
username: {
type: String,
trim: true
},
tags:[
{
title:String
}
]
});
exports.searchByTag = function(req, res) {
var tag = req.params.tag;
User.find({'tags.title':{ $regex: tag }}).distinct('tags.title', function(error, tags) {
res.json(tags);
});
Kept cracking & figured it out. This solution uses the lodash _.filter method.
searchTerm = 'abc'
//search the database for unique user.tag.title
User.find().distinct('tags.title', function(error, tagTitles) {
var results = _.filter(tagTitles, function(title){
//use lodash to filter out the the tagTitles array built by mongoose
//return = push the item into the results array based on regex match of searchTerm
return title.match(searchTerm);
});
//respond with the results array
res.json(results);
});

Resources