How to create and update referenced documents with mongoose? - node.js

I have this setup
var NotesSchema = new mongoose.Schema({
title: String,
category: [{ type: mongoose.ObjectId, ref: "Categories", default: [] }],
},{ timestamps: { createdAt: 'created_at' } });
var CategoriesSchema = new Schema({
name: {type: String, required: true}
})
var Notes = mongoose.model('Notes', NotesSchema);
var Cat = mongoose.model('Categories', CategoriesSchema);
If I want to create a new note and categories I do this
.get('/new', async (req, res) => {
var post1= {
title : "Post: books, thriller, crime and classics",
favorite : true,
categories:[ 'thriller', 'books']
}
try{
var note = post1.categories.map( (cat)=>{
var ca = new Cat({name: cat})
ca.save()
return ca._id
})
post1.category = note
const newNote = new Notes(post1);
const n = await newNote.save()
res.send(n)
} catch(error) {
console.error(error);
};
})
If I were to create a new note that has some new categories I'm stuck.
var post1= {
...
categories:[ 'thriller', 'books', 'classics']
}
'thriller' and 'books' already exist, but 'classics' doesn't.
I tried Cat.find({"name": {$in: post1.categories}}).exec() but I can't seem to look through that data.
All the example tutorials seem to just add one new entry at a time.

Inside your post1.categories.map callback you are creating the Cat documents and call save() on them but you do not await the returned promise. So Note is created before those promises are fulfilled and thus post1.category will be an empty array.
You can fix this by awaiting the save-promises of the categories:
const savePromises = post1.categories.map((cat) => {
const ca = new Cat({name: cat})
return ca.save();
})
const categoryIds = (await Promise.all(savePromises)).map(category => category._id);
post1.category = categoryIds;
// rest of your code

Related

Update multiple documents instance in MongoDb collection with Mongoose. save() is not a function

Mongoose newbe here. I got the following function to update the references (deleting them) in the document Post when a Tag is deleted. When I call my GraphQl API this is what I got:
message": "posts.save is not a function"
The function in my gql resolver:
async deleteTag(root, { id }, context) {
const posts = await Post.find();
const tag = await Tag.findById(id);
if(!tag){
const error = new Error('Tag not found!');
error.code = 404;
throw error;
}
posts?.forEach(async (post) => {
await post.tags.pull(id);
})
await posts.save()
await Tag.findByIdAndRemove(id);
return true;
}
This is the Post model:
const PostSchema = new Schema({
body: {
type: String,
required: true
},
tags: {
type: [Schema.Types.ObjectId],
ref: 'Tag',
required: false
},
});
and this is the Tag model:
const TagSchema = new Schema(
{
name: {
type: String,
required: true
},
},
{ timestamps: true }
);
Looks like I can't call the method save() on the array of objects returned by Exercise.find()
I used the same pattern in other functions, the difference is that there I used .findById()
Any solution? Advice and best practice advide are super welcome.
You have to save the posts individually:
posts?.forEach(async (post) => {
await post.tags.pull(id);
await post.save();
})
Or use Model.updateMany() combined with the $pull operator.
FWIW, you should probably limit the number of matching Post documents by selecting only documents that have the specific tag listed:
await Post.find({ 'tags._id' : id });

Not able to save my array in to mongodb database

I am new in backend development, creating a post API by using node js and MongoDB,
This is my schema model
// restaurantSchema.js
const mongoose = require('mongoose');
const foodSchema = new mongoose.Schema({
categoryName:{
type: String,
required: true
},
dish: [
{
dishName: {
type: String,
required: true
},
dishDescription: {
type: String
},
dishPrice: {
type: Number,
required: true
},
// dishImg: {
// type: String,
// },
dishRating: {
type: String
},
}
]
});
const restaurantSchema = new mongoose.Schema({
restaurantName: {
type: String,
},
restaurantRating:{
type: String
},
restaurantAddress: {
type: String
},
category: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'foodSchema'
}
]
})
const Restaurant = new mongoose.model('RESTAURANT', restaurantSchema);
module.exports = Restaurant;
here is the router code
// auth.js
const express = require('express');
const router = express.Router();
require('../db/conn');
const Restaurant = require('../model/restaurantSchema');
const app = express();
router.post('/post-food', async (req, res) => {
try {
console.log(req.body, "body")
const restaurant = new Restaurant({
restaurantName: req.body.restaurantName,
restaurantRating: req.body.restaurantRating,
restaurantAddress: req.body.restaurantAddress,
category: [
{
categoryName: req.body.categoryName
},
{
dishName: req.body.dishName,
dishDescription: req.body.dishDescription,
dishPrice: req.body.dishPrice,
dishRating: req.body.dishRating
}
]
});
await restaurant.save();
console.log(restaurant, "save")
res.status(201).json({ message: "food added successfully" })
} catch (err) {
console.log(err)
}
})
module.exports = router;
this code is going to the app.js (main file)
I am getting only this data while I comment out the category(array) in the auth.js file otherwise not able to save any data in the database file
[1]: https://i.stack.imgur.com/wKn9p.png
First of all this is a great question with very valid code, nicely done!
So, everything is correct in your code except you should declare your foodSchema as a mongoose model as well, just like you're doing so with this one
const Restaurant = new mongoose.model('RESTAURANT', restaurantSchema);
You don't need to use new, you can just omit it.
So basically next to this add the declaration for your foodSchema it should look something like this:
const Restaurant = mongoose.model('RESTAURANT', restaurantSchema);
const Food = mongoose.model('FOOD', foodSchema)
and for exporting them you could do something like this:
module.exports = {
Restaurant: Restaurant,
Food: Food
}
For importing any of these two in your router file you could use destructuring:
const { Restaurant } = require('../model/restaurantSchema')
I hope this helps, have a good day!
You have declared category as a list of mongoose typeId and inserting a list of object of type foodSchema. In your schema file after declaring foodSchema, create a model using that food schema.
const Food = mongoose.model('FOOD', foodSchema)
and now in restaurant schema the category will refer ref: 'Food'
mongoose expecting category as ["61407ce0b6c1fc83d896002e"] but you are providing
[{
categoryName:{
type: String,
required: true
},
dish:[...]
}]
export both Food and Restaurant from the schema file.
module.exports = Food;
for creating restaurant first create an Food
const food = new Food({
categoryName: req.body.categoryName,
dish: [ {
dishName: req.body.dishName,
dishDescription: req.body.dishDescription,
dishPrice: req.body.dishPrice,
dishRating: req.body.dishRating
} ]
})
let foodInserted = await food.save();
then the foodInserted._id to the category of restaurant.
const restaurant = new Restaurant({
restaurantName: req.body.restaurantName,
restaurantRating: req.body.restaurantRating,
restaurantAddress: req.body.restaurantAddress,
category: [foodInserted._id]
});
Follow this documentation

Mongoose is not creating collection name from variable

schema.js
****************************************************************
var nameOfCategory = "hello";
ecomm.createProductCollection = async (categoryName) =>{
nameOfCategory = categoryName;
}
const productSchema = new mongoose.Schema({
productName:{
type: String,
require: true
}
},
// { collection: nameOfCategory }
)
ecomm.productModel = new mongoose.model(nameOfCategory, productSchema, nameOfCategory)
*******************************************************************************************
controller.js
await ecomm.createProductCollection("someDynamicName")
await ecomm.productModel.create(product);
-----------------------------------------------------------------------
Expected Result: Collection created with name "someDynamicName".
Actual Result: Collection created with name "hello".
But while printing in console, nameOfCategory displays "someDynamicName"
This works out when the productSchema is created inside function.
But still could not find the reason why the code in question is not working.
schema.js
****************************************************************
ecomm.createProductCollection = async (categoryName) =>{
nameOfCategory = categoryName;
const productSchema = new mongoose.Schema({
productName:{
type: String,
require: true
}
},
// { collection: nameOfCategory }
)
ecomm.productModel = new mongoose.model(nameOfCategory, productSchema, nameOfCategory)
}

Mongoose schema inheritance and model populate

I have been trying this with the built in inheritance features of mongoose (rather than the extend plugin) but haven't been having much luck so far. This is a simplified example of code I am trying to use which exhibits the same problem. This is based on an expanded version of the mongoose documentation for schema inheritance using discriminators - http://mongoosejs.com/docs/api.html#model_Model.discriminator
var util = require('util');
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/problem');
var Schema = mongoose.Schema;
var ObjectId = Schema.ObjectId;
function BaseSchema() {
Schema.apply(this, arguments);
this.add({
name: String,
createdAt: Date
});
}
util.inherits(BaseSchema, Schema);
var BossStatusSchema = new Schema({
status: String
});
var BossStatus = mongoose.model('BossStatus', BossStatusSchema);
var PersonSchema = new BaseSchema();
var Person = mongoose.model('Person', PersonSchema);
var BossSchema = new BaseSchema({
department: String,
bossStatus: {
type: ObjectId,
ref: 'BossStatus'
}
});
var Boss = Person.discriminator('Boss', BossSchema);
Example code to add the documents:
var superBoss = new BossStatus({
status: 'super'
});
var normalBoss = new BossStatus({
status: 'normal'
});
var andy = new Person({
name: 'Andy'
});
var billy = new Boss({
name: 'Billy',
bossStatus: superBoss._id
});
var callback = function(err, result) {
console.dir(err);
console.dir(result);
};
superBoss.save(callback);
normalBoss.save(callback);
andy.save(callback);
billy.save(callback);
So when finding a record without populate:
Person
.findOne({
name: 'Billy'
})
.exec(callback);
The result is as expected, the bossStatus refers to an _id from the bossstatuses collection:
null
{ name: 'Billy',
bossStatus: 52a20ab0185a7f4530000001,
_id: 52a20ab0185a7f4530000004,
__v: 0,
__t: 'Boss' }
When adding the populate call:
Person
.findOne({
name: 'Billy'
})
.populate('bossStatus')
.exec(callback);
The resulting bossStatus property of the Person result is null:
null
{ name: 'Billy',
bossStatus: null,
_id: 52a20ab0185a7f4530000004,
__v: 0,
__t: 'Boss' }
EDIT:
Ok I've just put together what is probably a better example of what I'm trying to achieve, the schema structure lends itself more to a relational DB but hopefully makes the problem clearer.
var util = require('util');
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/problem');
var Schema = mongoose.Schema;
var ObjectId = Schema.ObjectId;
function BaseSchema() {
Schema.apply(this, arguments);
this.add({
name: {
type: String,
unique: true,
required: true
}
});
}
util.inherits(BaseSchema, Schema);
var DeviceSchema = new BaseSchema();
var LocalDeviceSchema = new BaseSchema({
driver: {
type: ObjectId,
ref: 'Driver'
}
});
var RemoteDeviceSchema = new BaseSchema({
networkAddress: {
type: ObjectId,
ref: 'NetworkAddress'
}
});
var DriverSchema = new Schema({
name: {
type: String,
unique: true,
required: true
}
});
var NetworkHostSchema = new Schema({
host: {
type: String,
unique: true,
required: true
}
});
var NetworkAddressSchema = new Schema({
networkHost: {
type: ObjectId,
ref: 'NetworkHost'
},
port: {
type: Number,
min: 1,
max: 65535
}
});
var Driver = mongoose.model('Driver', DriverSchema);
var NetworkHost = mongoose.model('NetworkHost', NetworkHostSchema);
var NetworkAddress = mongoose.model('NetworkAddress', NetworkAddressSchema);
var Device = mongoose.model('Device', DeviceSchema);
var LocalDevice = Device.discriminator('LocalDevice', LocalDeviceSchema);
var RemoteDevice = Device.discriminator('RemoteDevice', RemoteDeviceSchema);
var networkHost = new NetworkHost({
host: '192.168.2.1'
});
var networkAddress = new NetworkAddress({
networkHost: networkHost._id,
port: 3000
});
var remoteDevice = new RemoteDevice({
name: 'myRemoteDevice',
networkAddress: networkAddress._id
});
var driver = new Driver({
name: 'ftdi'
});
var localDevice = new LocalDevice({
name: 'myLocalDevice',
driver: driver._id
});
var callback = function(err, result) {
if(err) {
console.log(err);
}
console.dir(result);
};
/*
// Uncomment to save documents
networkHost.save(function() {
networkAddress.save(function() {
remoteDevice.save(callback);
});
});
driver.save(function() {
localDevice.save(callback);
});
*/
var deviceCallback = function(err, device) {
if(err) {
console.log(err);
}
switch(device.__t) {
case 'LocalDevice':
console.log('Would create a local device instance passing populated result');
break;
case 'RemoteDevice':
console.log('Would create a remote device instance passing populated result');
break;
}
};
Device
.findOne({name: 'myLocalDevice'})
.populate('driver')
.exec(deviceCallback);
The LocalDevice and RemoteDevice schemas could (and probably would) include other differences..
The switch would for example use a DeviceFactory or something to create the instances. My thinking was it should be possible to search the devices table for a device by 'name' and populate the collection references (if this is the correct terminology?) without having to specify the collection to search in - this was my understanding of the point of schema inheritance - or have I completely misunderstood?
Thanks for replies so far!
You are looking for a Boss, not a Person:
Boss
.findOne({
name: 'Billy'
})
.populate('bossStatus')
.exec(callback);
Looks like a bug. With debugging active, this is what's being shown for the population query:
Mongoose: people.findOne({ name: 'Billy' }) { fields: undefined }
Mongoose: people.find({ _id: { '$in': [ ObjectId("52a221ee639cc03d71000001") ] } }) { fields: undefined }
(the ObjectId shown is the one stored in bossStatus)
So Mongoose is querying the wrong collection (people instead of bossstatuses).
As #regretoverflow pointed out, if you're looking for a boss, use the Boss model and not the Person model.
If you do want to populate bossStatus through the Person model, you can explicitly state a model that needs to be searched for population:
.populate({
path : 'bossStatus',
model : 'BossStatus'
})
// or shorter but less clear:
// .populate('bossStatus', {}, 'BossStatus')
EDIT: (with your Device examples)
driver is part of LocalDeviceSchema, but you're querying the Device model, which has no notion of what driver is and populating driver within the context of a Device instance doesn't make sense to Mongoose.
Another possibility for populating each instance is to do it after you retrieved the document. You already have the deviceCallback function, and this will probably work:
var deviceCallback = function(err, device) {
if(err) {
console.log(err);
}
switch(device.__t) { // or `device.constructor.modelName`
case 'LocalDevice':
device.populate('driver', ...);
break;
case 'RemoteDevice':
device.populate('networkAddress', ...);
break;
}
};
The reason is that the document is already cast into the correct model there, something that apparently doesn't happen when you chain populate with the find.

How to set ObjectId as a data type in mongoose

Using node.js, mongodb on mongoHQ and mongoose. I'm setting a schema for Categories. I would like to use the document ObjectId as my categoryId.
var mongoose = require('mongoose');
var Schema = mongoose.Schema,
ObjectId = Schema.ObjectId;
var Schema_Category = new Schema({
categoryId : ObjectId,
title : String,
sortIndex : String
});
I then run
var Category = mongoose.model('Schema_Category');
var category = new Category();
category.title = "Bicycles";
category.sortIndex = "3";
category.save(function(err) {
if (err) { throw err; }
console.log('saved');
mongoose.disconnect();
});
Notice that I don't provide a value for categoryId. I assumed mongoose will use the schema to generate it but the document has the usual "_id" and not "categoryId". What am I doing wrong?
Unlike traditional RBDMs, mongoDB doesn't allow you to define any random field as the primary key, the _id field MUST exist for all standard documents.
For this reason, it doesn't make sense to create a separate uuid field.
In mongoose, the ObjectId type is used not to create a new uuid, rather it is mostly used to reference other documents.
Here is an example:
var mongoose = require('mongoose');
var Schema = mongoose.Schema,
ObjectId = Schema.ObjectId;
var Schema_Product = new Schema({
categoryId : ObjectId, // a product references a category _id with type ObjectId
title : String,
price : Number
});
As you can see, it wouldn't make much sense to populate categoryId with a ObjectId.
However, if you do want a nicely named uuid field, mongoose provides virtual properties that allow you to proxy (reference) a field.
Check it out:
var mongoose = require('mongoose');
var Schema = mongoose.Schema,
ObjectId = Schema.ObjectId;
var Schema_Category = new Schema({
title : String,
sortIndex : String
});
Schema_Category.virtual('categoryId').get(function() {
return this._id;
});
So now, whenever you call category.categoryId, mongoose just returns the _id instead.
You can also create a "set" method so that you can set virtual properties, check out this link
for more info
I was looking for a different answer for the question title, so maybe other people will be too.
To set type as an ObjectId (so you may reference author as the author of book, for example), you may do like:
const Book = mongoose.model('Book', {
author: {
type: mongoose.Schema.Types.ObjectId, // here you set the author ID
// from the Author colection,
// so you can reference it
required: true
},
title: {
type: String,
required: true
}
});
My solution on using ObjectId
// usermodel.js
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const ObjectId = Schema.Types.ObjectId
let UserSchema = new Schema({
username: {
type: String
},
events: [{
type: ObjectId,
ref: 'Event' // Reference to some EventSchema
}]
})
UserSchema.set('autoIndex', true)
module.exports = mongoose.model('User', UserSchema)
Using mongoose's populate method
// controller.js
const mongoose = require('mongoose')
const User = require('./usermodel.js')
let query = User.findOne({ name: "Person" })
query.exec((err, user) => {
if (err) {
console.log(err)
}
user.events = events
// user.events is now an array of events
})
The solution provided by #dex worked for me. But I want to add something else that also worked for me: Use
let UserSchema = new Schema({
username: {
type: String
},
events: [{
type: ObjectId,
ref: 'Event' // Reference to some EventSchema
}]
})
if what you want to create is an Array reference. But if what you want is an Object reference, which is what I think you might be looking for anyway, remove the brackets from the value prop, like this:
let UserSchema = new Schema({
username: {
type: String
},
events: {
type: ObjectId,
ref: 'Event' // Reference to some EventSchema
}
})
Look at the 2 snippets well. In the second case, the value prop of key events does not have brackets over the object def.
You can directly define the ObjectId
var Schema = new mongoose.Schema({
categoryId : mongoose.Schema.Types.ObjectId,
title : String,
sortIndex : String
})
Note: You need to import the mongoose module
Another possible way is to transform your _id to something you like.
Here's an example with a Page-Document that I implemented for a project:
interface PageAttrs {
label: string
// ...
}
const pageSchema = new mongoose.Schema<PageDoc>(
{
label: {
type: String,
required: true
}
// ...
},
{
toJSON: {
transform(doc, ret) {
// modify ret directly
ret.id = ret._id
delete ret._id
}
}
}
)
pageSchema.statics.build = (attrs: PageAttrs) => {
return new Page({
label: attrs.label,
// ...
})
}
const Page = mongoose.model<PageDoc, PageModel>('Page', pageSchema)
Now you can directly access the property 'id', e.g. in a unit test like so:
it('implements optimistic concurrency', async () => {
const page = Page.build({
label: 'Root Page'
// ...
})
await page.save()
const firstInstance = await Page.findById(page.id)
const secondInstance = await Page.findById(page.id)
firstInstance!.set({ label: 'Main Page' })
secondInstance!.set({ label: 'Home Page' })
await firstInstance!.save()
try {
await secondInstance!.save()
} catch (err) {
console.error('Error:', err)
return
}
throw new Error('Should not reach this point')
})

Resources