Add a deeply embedded document conditionally if unique field value - node.js

I have following model and schemas in mongoose :
const Doc = mongoose.model(
"Doc",
new mongoose.Schema({
name: String,
subDocs: [SubDoc],
})
);
const SubDoc = new mongoose.Schema({
name: String,
subSubDocs: [SubSubDoc],
});
const SubSubDoc = new mongoose.Schema({
name: String,
});
This code adds a uniquely named embedded SubDoc to the parent Doc. By "uniquely named" I mean it will not create a subdocument if another subdocument in the parent has the same value for "name":
const createUniqueSubDoc = (docId, name) => {
return db.Doc.findOneAndUpdate(
{ _id: docId, "subDocs.name": { $ne: name } },
{
$push: {
subDocs: {
name: name,
},
},
},
{ new: true, useFindAndModify: false }
);
};
const run = async (doc) => {
let doc = await db.Doc.create({name:"Main document"})
createUniqueSubDoc(doc._id, "sub document name");
};
run();
addToSet doesn't work with dates or id's so I'm $ne as suggested here.
But how do I add a uniquely named embedded SubSubDoc to the already embedded SubDoc's subSubDoc field? You can't query the embedded SubDoc with findOneAndUpdate because it's only a schema not a model like the Doc model.

using MongooseArray methods:
const createUniqueSubSubDoc = async function (docId, subDocId, subSubDoc) {
const doc = await db.Doc.findById(docId);
const subDoc = doc.subDocs.id(subDocId);
if (subDoc.subSubDocs.find((ssd) => ssd.name == subSubDoc.name))
return "oops! you already gave another SubSubDoc this name";
subDoc.subSubDocs.push(subSubDoc);
await doc.save();
return doc.subDocs.id(subDocId);
};
createSubSubDoc(
"5f56d9621222bbcc3bf4ee41", // the id for a Doc
"5f56d9631222bbcc3bf4ee42", // the id for a SubDoc of that Doc
{ name: "subSubDoc name" } // the object for the new SubSubDoc
);
git repo

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

Populate array inside object in mongoose

I have a company model which looks like this:
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const CompanySchema = new Schema(
{
companyName: String,
settings: {
type: {
priceVisible: Boolean,
allowPickupAddressAddition: Boolean,
paymentMethodsAvailable: [
{ type: Schema.Types.ObjectId, ref: "PaymentMethod" },
],
},
},
}
);
const Company = mongoose.model("Company", CompanySchema);
module.exports = Company;
And I want to populate the values store in paymentMethodsAvailable array. Here is relevant controller code:
const company = await Company.findOne({ _id: id }).populate([
{
path: "settings",
populate: [{path: "paymentMethodsAvailable"}]
},
]);
But this doesn't work as expected. I can see that it might be trying to populate settings object, and fails there. Is there a way in which I can tell mongoose to populate settings.paymentMethodsAvailable ?
Try this
const company = await Company.findOne({ _id: id }).populate(
"settings.paymentMethodsAvailable"
);
You can find more examples in the documentation. I was using this section as a reference https://mongoosejs.com/docs/populate.html#populating-maps
Mongoose provides clear syntax
The following code will work fine
const company = await Company.findOne({ _id: id }).populate(
"settings.type.paymentMethodsAvailable"
);
if(!company) {
// if there is no company with that id
// this acully not error it's simply
// return `null` to tell you company not found.
res.status(404).send()
}
Also: you can go further and populate specific fields inside settings.paymentMethodsAvailable
const company = await Company.findOne({ _id: id }).populate(
"settings.type.paymentMethodsAvailable",
"field1 filed2 filed3"
);

How to create and update referenced documents with mongoose?

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

mongoose filter by multiple conditions and execute to update data

I am wondering what would be the best approach to make schema functions using mongoose. I have never used this so the way I think is somewhat limited, same goes for looking for docs, without knowing what's available, is not very efficient.
Through docs I found that either using findOneAndUpdate might solve the problem; but there are some constraints.
Here is the code I am planning to run:
models/Bookmark.js
const mongoose = require('mongoose')
const bookmarkItemSchema = new mongoose.Schema({
restaurantId: String,
cachedAttr: {
name: String,
latitude: Number,
longitude: Number,
},
})
const bookmarkListSchema = new mongoose.Schema({
listName: String,
items: [bookmarkItemSchema],
})
const bookmarkSchema = new mongoose.Schema({
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
},
lists: [bookmarkListSchema],
})
// const add = (lists, userId) => {
// let bookmark = Bookmark.findOne({userId})
// bookmark.lists.listName === lists.listName //current, new
// ? bookmark.lists.items.push(lists.items)
// : bookmark.lists.push(lists)
// return bookmark
// }
mongoose.model('Bookmark', bookmarkSchema)
Routes/bookmark.js
router.post('/bookmarks', async (req, res) => {
const {lists} = req.body
console.log(lists)
if (!lists) {
return res.status(422).send({error: 'You must provide lists'})
}
let bookmark = Bookmark.findOne({"userId": req.user._id})
if (bookmark.lists.listName === lists.listName){
let item = lists.items
bookmark.lists.items.push(item)
await bookmark.save()
res.send(bookmark)
}
try {
// const bookmark = Bookmark.add(lists, req.user._id, obj)
// await bookmark.save()
// res.send(bookmark)
let bookmark = Bookmark.findOne({"userId": req.user._id})
if (bookmark.lists.listName === lists.listName){ // THIS IS UNDEFINED. How to get this object?
let item = lists.items
bookmark.lists.items.push(item)
await bookmark.save()
res.send(bookmark)
}
} catch (e) {
res.status(422).send({error: e.message})
}
})
The req.body looks like this:
{
"lists": {
"listName": "My Saved List",
"items": {
"restaurantId": "abcdefg",
"cachedAttr": {
"name": "abcdefg",
"latitude": 200,
"longitude": 200
}
}
}
}
Basically what I commented out in the models/Bookmark.js file is what I would really like to do.
If the userId's list name already exists, then I would like to just add an item to the list.
Otherwise, I would like to add a new list to the object.
What is the best approach for doing this? Is there a straight forward mongoose api that I could use for this problem? or do I need to make two separated function that would handle each case and make that as schema methods and handle it in the routes file?

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