Create subsubdocs in Mongoose with Nested Schema - node.js

I have a User Schema that includes nested Schemas - overall three layers deep. I have no problem adding to Polls, but cannot add Inputs for the life of me. I have tried to get a user then handle the data like regular json (such as getting the index of the polls array and then pushing to the nested input array). I have tried creating a new input and pushing it to the inputs array.
The following code works perfectly for creating a new poll on a given user:
const poll = user.polls.create({});
user.polls.push(poll);
user.save(function(err, data) {
//more stuff here
})
However, when I try the same method on polls, I cannot save to the DB.
const poll = user.polls.id(<some _id>);
const input = poll.create({}); // or user.polls.id(<some _id>).create({});
poll.inputs.push(input);
I have gone back and forth and tried several things and cannot seem to get this to work given my current data structure. I have read several posts on SO as well as other online sources but most give examples for the first embedded subdoc, not the subdoc of the subdoc. Any help you can provide would be greatly appreciated. Thanks. I'm thinking of scrapping Mongoose on this project and just using the mongodb package for clearer control over what is going on with my data.
Here's my current code:
From User.js
const InputSchema = new Schema({
[ALL SORTS OF VARIOUS DATA HERE]
});
const PollSchema = new Schema({
inputs: [InputSchema],
});
const UserSchema = new Schema({
polls: [PollSchema]
});
module.exports = mongoose.model('User', UserSchema);
From controllers.js
const User = require('../models/User');
router.post('/polls/inputs/add/:creatorId', function(req, res) {
let title = req.body.option.title,
order = req.body.option.order,
voters= [];
User.findOne({ creatorId: req.params.creatorId })
.then(user => {
const input = {order, title, voters};
user.polls.id(req.body.pollId).inputs.push(input);
user.save(function(err, data) {
if (err) {
res.statusCode = 500;
return res.json({ title: 'Save Error', message: err });
}
console.log('Success!');
res.json({ input: input });
});
}).catch(err => {
res.statusCode = 500;
return res.json({ title: 'DB Error', message: err });
});;
});

Mongoose isn't able to track changes in subobjects like this, so the field must be marked explicitely as needing to be updated like so:
user.markModified('polls')

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...

Getting started with express

I am just getting started learning node.js and express, so forgive me if my question uses the wrong terminology. Hey, gotta start somewhere I guess. So I am following an online tutorial and it aint doing what its supposed to do. (I've got soapui and postman installed, also just learning the ropes) So to the code:
// Defined store route
gameRoutes.route('/add').post(function(req, res) {
let game = new Game(req.body);
game.save()
.then(game => {
res.status(200).json({
'game': 'CoGamein added successfully'
});
})
.catch(err => {
res.status(400).send("unable to save to database");
});
});
Tried using the insert code widget but failed miserably, sorry! Anyway what's happening is the data is added to the database but the res.status(200).json({'game': 'CoGamein added successfully'}); is not firing, and VScode gives me a vague error that let game = new Game(req.body); data is not read! Any ideas whats going on?
// Game.js
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
// Define collection and schema for Games
let Game = new Schema({
name: {
type: String
},
price: {
type: Number
}
},{
collection: 'games'
});
module.exports = mongoose.model('Game', Game);
Try this
gameRoutes.route('/add').post(function (req, res) {
let game = new Game(req.body);
game.save()
.then(game => {
res.send({
game: 'CoGamein added successfully'
});
})
.catch(err => {
res.status(400).send("unable to save to database");
});
});
200 is the default success status. You don't need to mention that.

Nodejs Duplicate Fields

I'm using a POST route to post data on a user's progress. I'm looking for a way to check if theres duplicate fields when posting, so I don't post multiple results
My route:
api.post('/progress', (req, res) => {
let progress = new Progress();
progress.user_id = req.body.user_id;
progress.level = req.body.level;
progress.save(function (err) {
if (err) {
res.send(err);
}
res.json({
message: 'progress saved!'
});
});
});
My Model
import mongoose from 'mongoose';
let Schema = mongoose.Schema;
let progressSchema = new Schema({
user_id: String,
level: String,
});
var levels = mongoose.model('Progress', progressSchema);
module.exports = mongoose.model('Progress', progressSchema);
Are you using MongoDB? If so, you can use mongoose module and add
unique: true,
to the field's attributes in your Progress schema. Check out the docs. Hope this helps.

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);

Dynamically create collection with Mongoose

I want to give users the ability to create collections in my Node app. I have really only seen example of hard coding in collections with mongoose. Anyone know if its possible to create collections dynamically with mongoose? If so an example would be very helpful.
Basically I want to be able to store data for different 'events' in different collections.
I.E.
Events:
event1,
event2,
...
eventN
Users can create there own custom event and store data in that collection. In the end each event might have hundreds/thousands of rows. I would like to give users the ability to perform CRUD operations on their events. Rather than store in one big collection I would like to store each events data in a different collection.
I don't really have an example of what I have tried as I have only created 'hard coded' collections with mongoose. I am not even sure I can create a new collection in mongoose that is dynamic based on a user request.
var mongoose = require('mongoose');
mongoose.connect('localhost', 'events');
var schema = mongoose.Schema({ name: 'string' });
var Event1 = mongoose.model('Event1', schema);
var event1= new Event1({ name: 'something' });
event1.save(function (err) {
if (err) // ...
console.log('meow');
});
Above works great if I hard code 'Event1' as a collection. Not sure I create a dynamic collection.
var mongoose = require('mongoose');
mongoose.connect('localhost', 'events');
...
var userDefinedEvent = //get this from a client side request
...
var schema = mongoose.Schema({ name: 'string' });
var userDefinedEvent = mongoose.model(userDefinedEvent, schema);
Can you do that?
I believe that this is a terrible idea to implement, but a question deserves an answer. You need to define a schema with a dynamic name that allows information of 'Any' type in it. A function to do this may be a little similar to this function:
var establishedModels = {};
function createModelForName(name) {
if (!(name in establishedModels)) {
var Any = new Schema({ any: Schema.Types.Mixed });
establishedModels[name] = mongoose.model(name, Any);
}
return establishedModels[name];
}
Now you can create models that allow information without any kind of restriction, including the name. I'm going to assume an object defined like this, {name: 'hello', content: {x: 1}}, which is provided by the 'user'. To save this, I can run the following code:
var stuff = {name: 'hello', content: {x: 1}}; // Define info.
var Model = createModelForName(name); // Create the model.
var model = Model(stuff.content); // Create a model instance.
model.save(function (err) { // Save
if (err) {
console.log(err);
}
});
Queries are very similar, fetch the model and then do a query:
var stuff = {name: 'hello', query: {x: {'$gt': 0}}}; // Define info.
var Model = createModelForName(name); // Create the model.
model.find(stuff.query, function (err, entries) {
// Do something with the matched entries.
});
You will have to implement code to protect your queries. You don't want the user to blow up your db.
From mongo docs here: data modeling
In certain situations, you might choose to store information in
several collections rather than in a single collection.
Consider a sample collection logs that stores log documents for
various environment and applications. The logs collection contains
documents of the following form:
{ log: "dev", ts: ..., info: ... } { log: "debug", ts: ..., info: ...}
If the total number of documents is low you may group documents into
collection by type. For logs, consider maintaining distinct log
collections, such as logs.dev and logs.debug. The logs.dev collection
would contain only the documents related to the dev environment.
Generally, having large number of collections has no significant
performance penalty and results in very good performance. Distinct
collections are very important for high-throughput batch processing.
Say I have 20 different events. Each event has 1 million entries... As such if this is all in one collection I will have to filter the collection by event for every CRUD op.
I would suggest you keep all events in the same collection, especially if event names depend on client code and are thus subject to change. Instead, index the name and user reference.
mongoose.Schema({
name: { type: String, index: true },
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', index: true }
});
Furthermore I think you came at the problem a bit backwards (but I might be mistaken). Are you finding events within the context of a user, or finding users within the context of an event name? I have a feeling it's the former, and you should be partitioning on user reference, not the event name in the first place.
If you do not need to find all events for a user and just need to deal with user and event name together you could go with a compound index:
schema.index({ user: 1, name: 1 });
If you are dealing with millions of documents, make sure to turn off auto index:
schema.set('autoIndex', false);
This post has interesting stuff about naming collections and using a specific schema as well:
How to access a preexisting collection with Mongoose?
You could try the following:
var createDB = function(name) {
var connection = mongoose.createConnection(
'mongodb://localhost:27017/' + name);
connection.on('open', function() {
connection.db.collectionNames(function(error) {
if (error) {
return console.log("error", error)
}
});
});
connection.on('error', function(error) {
return console.log("error", error)
});
}
It is important that you get the collections names with connection.db.collectionNames, otherwise the Database won't be created.
This method works best for me , This example creates dynamic collection for each users , each collection will hold only corresponding users information (login details), first declare the function dynamicModel in separate file : example model.js
/* model.js */
'use strict';
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
function dynamicModel(suffix) {
var addressSchema = new Schema(
{
"name" : {type: String, default: '',trim: true},
  "login_time" : {type: Date},
"location" : {type: String, default: '',trim: true},
}
);
return mongoose.model('user_' + suffix, addressSchema);
}
module.exports = dynamicModel;
In controller File example user.js,first function to create dynamic collection and second function to save data to a particular collection
/* user.js */
var mongoose = require('mongoose'),
function CreateModel(user_name){//function to create collection , user_name argument contains collection name
var Model = require(path.resolve('./model.js'))(user_name);
}
function save_user_info(user_name,data){//function to save user info , data argument contains user info
var UserModel = mongoose.model(user_name) ;
var usermodel = UserModel(data);
usermodel.save(function (err) {
if (err) {
console.log(err);
} else {
console.log("\nSaved");
}
});
}
yes we can do that .I have tried it and its working.
REFERENCE CODE:
app.post("/",function(req,res){
var Cat=req.body.catg;
const link= req.body.link;
const rating=req.body.rating;
Cat=mongoose.model(Cat,schema);
const item=new Cat({
name:link,
age:rating
});
item.save();
res.render("\index");
});
I tried Magesh varan Reference Code ,
and this code works for me
router.post("/auto-create-collection", (req, res) => {
var reqData = req.body; // {"username":"123","password":"321","collectionName":"user_data"}
let userName = reqData.username;
let passWord = reqData.password;
let collectionName = reqData.collectionName;
// create schema
var mySchema = new mongoose.Schema({
userName: String,
passWord: String,
});
// create model
var myModel = mongoose.model(collectionName, mySchema);
const storeData = new myModel({
userName: userName,
passWord: passWord,
});
storeData.save();
res.json(storeData);
});
Create a dynamic.model.ts access from some where to achieve this feature.
import mongoose, { Schema } from "mongoose";
export default function dynamicModelName(collectionName: any) {
var dynamicSchema = new Schema({ any: Schema.Types.Mixed }, { strict: false });
return mongoose.model(collectionName, dynamicSchema);
}
Create dynamic model
import dynamicModelName from "../models/dynamic.model"
var stuff = { name: 'hello', content: { x: 1 } };
var Model = await dynamicModelName('test2')
let response = await new Model(stuff).save();
return res.send(response);
Get the value from the dynamic model
var Model = dynamicModelName('test2');
let response = await Model.find();
return res.send(response);

Resources