How to remove multiple references when removing many documents? - javascript

I'm removing a document from collection 'Component' to which there are references in collection 'Space'.
Upon removal of document from Component, I remove the reference to that document from Space.
There is another collection 'ListItem', to which there are references in 'Space'. As I delete the Component, I want to delete an unknown amount of documents from 'ListItem' and then delete all these references from 'Space'. I was able to delete the documents inside 'ListItem' but not the references in Space.
I couldn't figure out how to use hooks with this. I now understand this would only work for removing a single reference. If I could somehow, per instance of ListItem that gets removed, pull ListItem.ListId from Space.listItems, it might work? $pullAll seems to be the most promising potential solution I've found thus far. Then I'd have to query Space and find all matching ids from Space.listItems and store them in an array?
ListItemsSchema.pre('deleteMany', function(next) {
var list = this;
var id = this.ListId;
var spaceId = this.SpaceId;
list.model('Space').update(
{ _id: spaceId},
{$pull: {listItems: id}},
{multi: true},
next
);
// i'm removing ListItem.ListId from Space.listItems
})
I couldn't figure out how to solve it as I remove the ListItems. Same issue that the pull method is wrong.
router.delete('/DeleteComponent/:id', function(req, res) {
let id = req.params.id;
let spaceID = req.body.CurrentSpaceId;
let listArray = [];
Component.findOneAndRemove({ _id: id}).exec(function(err, removed) {
Space.findOneAndUpdate(
{ _id: spaceID },
{ $pull: {components: id} },
{ new: true },
function(err, removedFromSpace) {
if (err) { console.log(err) }
res.status(200).send(removedFromSpace);
})
});
// delete all items where id == component/list id
ListItem.deleteMany({ ListId: id}).exec(function(err, results) {
if (err) {
return console.log(err);
} else {
console.log("ListItems successfully removed.", results);
}
Space.update(
{ _id: spaceID},
{$pull: {listItems: id}},
{multi: true},
function(err, removedFromSpace) {
if (err) { console.log(err) }
res.status(200).send(removedFromSpace);
}
);
});
});
I've tried a few more things but these have been my best attempts so far.
Space's schema:
let spaceSchema = new mongoose.Schema({
components: [{type: Schema.Types.ObjectId, ref: 'Component'}],
listItems: [{type: Schema.Types.ObjectId, ref: 'ListItem'}],
});
Component's schema:
let componentSchema = new mongoose.Schema({
...
})
ListItem's schema:
let ListItemsSchema = new mongoose.Schema({
ListItem: String,
// ListId is equivalent to Component's _id
ListId: String,
SpaceId: String
});
Expected result is that when a component is deleted, then also ListItems are deleted along with references to them in Space. Currently I'm unable to do the last step of deleting the references for ListItems in Space.

I ended up solving the problem through making structural changes to the database schemas, ending up with a generally cleaner and more elegant solution.
I moved the references listItems: [{type: Schema.Types.ObjectId, ref: 'ListItem'}] to the componentSchema, so when a component is deleted, all the references to associated ListItem documents are also deleted, without having to worry about accessing Space or iterating over values to dynamically create arrays. After that, all ListItem documents where ListItem.ComponentId matches the id of the recently deleted component are deleted.
// Delete component and remove from Spaces
router.delete('/DeleteComponent/:id', function(req, res) {
let id = req.params.id;
let spaceID = req.body.CurrentSpaceId;
Component.findOneAndRemove({ _id: id}).exec(function(err, removed) {
Space.findOneAndUpdate(
{ _id: spaceID },
{ $pull: {components: id} },
{ new: true },
function(err, removedFromSpace) {
if (err) { console.log(err) }
res.status(200).send(removedFromSpace);
})
});
// delete all items where id == component id
ListItem.deleteMany({ ComponentId: id}).exec(function(err, results) {
if (err) {
return console.log(err);
} else {
console.log("ListItems successfully removed.", results);
}
});
});
I did make progress towards solving the original technical challenge as well. My suggestion would be to find a way to gather all the IDs you want to remove from a reference and put them in an array, and then you can use $pullAll to remove them from references:
let myIdsArray = [id1, id2, id3];
Collection.update( {_id: id}, { $pullAll: {field: myIdsArray } } );
However, as far as I can tell, unless you have a very good reason for your references to exist in such a disconnected way, it makes more sense to use the much simpler solution, especially when using MongoDB or any other non-relational database.

Related

Mongoose: Remove referencing ObjectIds using deleteMany hook

My schema is like the following:
const Author = new Schema({
name: String,
posts: [{ type: mongoose.Schema.Types.ObjectId, ref: "Post" }]
});
const Post = new Schema({
text: String,
author: { type: mongoose.Schema.Types.ObjectId, ref: "Author" }
});
If I use deleteMany() to delete a bunch of posts, what is the best way to remove the ObjectId entries from the Author's object?
I tried to use the deleteMany pre hook, however it only passes how many doc are deleted and not the actual ObjectIds of the posts.
You could create the query object that is used for the deleteMany, and instead use it to query for the documents via find. That will give you all of the targeted documents, which you can then delete via deleteMany.
Afterwards, if the deleted count matches the length of fetched documents, then you can safely remove all from the author. If the total deleted does not match the length of fetched documents, then you can iterate through all initially-fetched documents, attempt to fetch it, and if it's not found, then that means it's deleted and you can safely remove from author.
Example in typescript:
const queryObj = { foo: 'bar' };
const postsCollection = mongoDb.getCollection<{ _id: ObjectId }>('posts');
const postsToDelete = await postsCollection.find(queryObj).toArray();
const deleteManyResult = await postsCollection.deleteMany(queryObj);
if (deleteManyResult.deletedCount === postsToDelete.length) {
for (const deletedPost of postsToDelete) {
// delete from author
}
} else {
for (const potentiallyDeletedPost of postsToDelete) {
const post = await postsCollection.findOne({ _id: potentiallyDeletedPost._id });
if (!post) {
// delete from author
} else {
// handle non-deleted post
}
}
}

How to make custom incremental id or number in MongoDB? [duplicate]

According to this mongodb article it is possible to auto increment a field and I would like the use the counters collection way.
The problem with that example is that I don't have thousands of people typing the data in the database using the mongo console. Instead I am trying to use mongoose.
So my schema looks something like this:
var entitySchema = mongoose.Schema({
testvalue:{type:String,default:function getNextSequence() {
console.log('what is this:',mongoose);//this is mongoose
var ret = db.counters.findAndModify({
query: { _id:'entityId' },
update: { $inc: { seq: 1 } },
new: true
}
);
return ret.seq;
}
}
});
I have created the counters collection in the same database and added a page with the _id of 'entityId'. From here I am not sure how to use mongoose to update that page and get the incrementing number.
There is no schema for counters and I would like it to stay that way because this is not really an entity used by the application. It should only be used in the schema(s) to auto increment fields.
Here is an example how you can implement auto-increment field in Mongoose:
var CounterSchema = Schema({
_id: {type: String, required: true},
seq: { type: Number, default: 0 }
});
var counter = mongoose.model('counter', CounterSchema);
var entitySchema = mongoose.Schema({
testvalue: {type: String}
});
entitySchema.pre('save', function(next) {
var doc = this;
counter.findByIdAndUpdate({_id: 'entityId'}, {$inc: { seq: 1} }, function(error, counter) {
if(error)
return next(error);
doc.testvalue = counter.seq;
next();
});
});
You can use mongoose-auto-increment package as follows:
var mongoose = require('mongoose');
var autoIncrement = require('mongoose-auto-increment');
/* connect to your database here */
/* define your CounterSchema here */
autoIncrement.initialize(mongoose.connection);
CounterSchema.plugin(autoIncrement.plugin, 'Counter');
var Counter = mongoose.model('Counter', CounterSchema);
You only need to initialize the autoIncrement once.
The most voted answer doesn't work. This is the fix:
var CounterSchema = new mongoose.Schema({
_id: {type: String, required: true},
seq: { type: Number, default: 0 }
});
var counter = mongoose.model('counter', CounterSchema);
var entitySchema = mongoose.Schema({
sort: {type: String}
});
entitySchema.pre('save', function(next) {
var doc = this;
counter.findByIdAndUpdateAsync({_id: 'entityId'}, {$inc: { seq: 1} }, {new: true, upsert: true}).then(function(count) {
console.log("...count: "+JSON.stringify(count));
doc.sort = count.seq;
next();
})
.catch(function(error) {
console.error("counter error-> : "+error);
throw error;
});
});
The options parameters gives you the result of the update and it creates a new document if it doesn't exist.
You can check here the official doc.
And if you need a sorted index check this doc
So combining multiple answers, this is what I ended up using:
counterModel.js
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
const counterSchema = new Schema(
{
_id: {type: String, required: true},
seq: { type: Number, default: 0 }
}
);
counterSchema.index({ _id: 1, seq: 1 }, { unique: true })
const counterModel = mongoose.model('counter', counterSchema);
const autoIncrementModelID = function (modelName, doc, next) {
counterModel.findByIdAndUpdate( // ** Method call begins **
modelName, // The ID to find for in counters model
{ $inc: { seq: 1 } }, // The update
{ new: true, upsert: true }, // The options
function(error, counter) { // The callback
if(error) return next(error);
doc.id = counter.seq;
next();
}
); // ** Method call ends **
}
module.exports = autoIncrementModelID;
myModel.js
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
const autoIncrementModelID = require('./counterModel');
const myModel = new Schema({
id: { type: Number, unique: true, min: 1 },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date },
someOtherField: { type: String }
});
myModel.pre('save', function (next) {
if (!this.isNew) {
next();
return;
}
autoIncrementModelID('activities', this, next);
});
module.exports = mongoose.model('myModel', myModel);
Attention!
As hammerbot and dan-dascalescu pointed out this does not work if you remove documents.
If you insert 3 documents with id 1, 2 and 3 - you remove 2 and insert another a new one it'll get 3 as id which is already used!
In case you don't ever remove documents, here you go:
I know this has already a lot of answers, but I would share my solution which is IMO short and easy understandable:
// Use pre middleware
entitySchema.pre('save', function (next) {
// Only increment when the document is new
if (this.isNew) {
entityModel.count().then(res => {
this._id = res; // Increment count
next();
});
} else {
next();
}
});
Make sure that entitySchema._id has type:Number.
Mongoose version: 5.0.1.
This problem is sufficiently complicated and there are enough pitfalls that it's best to rely on a tested mongoose plugin.
Out of the plethora of "autoincrement" plugins at http://plugins.mongoosejs.io/, the best maintained and documented (and not a fork) is mongoose sequence.
I've combined all the (subjectively and objectively) good parts of the answers, and came up with this code:
const counterSchema = new mongoose.Schema({
_id: {
type: String,
required: true,
},
seq: {
type: Number,
default: 0,
},
});
// Add a static "increment" method to the Model
// It will recieve the collection name for which to increment and return the counter value
counterSchema.static('increment', async function(counterName) {
const count = await this.findByIdAndUpdate(
counterName,
{$inc: {seq: 1}},
// new: return the new value
// upsert: create document if it doesn't exist
{new: true, upsert: true}
);
return count.seq;
});
const CounterModel = mongoose.model('Counter', counterSchema);
entitySchema.pre('save', async function() {
// Don't increment if this is NOT a newly created document
if(!this.isNew) return;
const testvalue = await CounterModel.increment('entity');
this.testvalue = testvalue;
});
One of the benefits of this approach is that all the counter related logic is separate. You can store it in a separate file and use it for multiple models importing the CounterModel.
If you are going to increment the _id field, you should add its definition in your schema:
const entitySchema = new mongoose.Schema({
_id: {
type: Number,
alias: 'id',
required: true,
},
<...>
});
test.pre("save",function(next){
if(this.isNew){
this.constructor.find({}).then((result) => {
console.log(result)
this.id = result.length + 1;
next();
});
}
})
I didn't wan to use any plugin (an extra dependencie, initializing the mongodb connection apart from the one I use in the server.js, etc...) so I did an extra module, I can use it at any schema and even, I'm considering when you remove a document from the DB.
module.exports = async function(model, data, next) {
// Only applies to new documents, so updating with model.save() method won't update id
// We search for the biggest id into the documents (will search in the model, not whole db
// We limit the search to one result, in descendant order.
if(data.isNew) {
let total = await model.find().sort({id: -1}).limit(1);
data.id = total.length === 0 ? 1 : Number(total[0].id) + 1;
next();
};
};
And how to use it:
const autoincremental = require('../modules/auto-incremental');
Work.pre('save', function(next) {
autoincremental(model, this, next);
// Arguments:
// model: The model const here below
// this: The schema, the body of the document you wan to save
// next: next fn to continue
});
const model = mongoose.model('Work', Work);
module.exports = model;
Hope it helps you.
(If this Is wrong, please, tell me. I've been having no issues with this, but, not an expert)
Here is a proposal.
Create a separate collection to holds the max value for a model collection
const autoIncrementSchema = new Schema({
name: String,
seq: { type: Number, default: 0 }
});
const AutoIncrement = mongoose.model('AutoIncrement', autoIncrementSchema);
Now for each needed schema, add a pre-save hook.
For example, let the collection name is Test
schema.pre('save', function preSave(next) {
const doc = this;
if (doc.isNew) {
const nextSeq = AutoIncrement.findOneAndUpdate(
{ name: 'Test' },
{ $inc: { seq: 1 } },
{ new: true, upsert: true }
);
nextSeq
.then(nextValue => doc[autoIncrementableField] = nextValue)
.then(next);
}
else next();
}
As findOneAndUpdate is an atomic operation, no two updates will return same seq value. Thus each of your insertion will get an incremental seq regardless of number of concurrent insertions. Also this can be extended to more complex auto incremental logic and the auto increment sequence is not limited to Number type
This is not a tested code. Test before you use until I make a plugin for mongoose.
Update I found that this plugin implemented related approach.
The answers seem to increment the sequence even if the document already has an _id field (sort, whatever). This would be the case if you 'save' to update an existing document. No?
If I'm right, you'd want to call next() if this._id !== 0
The mongoose docs aren't super clear about this. If it is doing an update type query internally, then pre('save' may not be called.
CLARIFICATION
It appears the 'save' pre method is indeed called on updates.
I don't think you want to increment your sequence needlessly. It costs you a query and wastes the sequence number.
I had an issue using Mongoose Document when assigning value to Schema's field through put(). The count returns an Object itself and I have to access it's property.
I played at #Tigran's answer and here's my output:
// My goal is to auto increment the internalId field
export interface EntityDocument extends mongoose.Document {
internalId: number
}
entitySchema.pre<EntityDocument>('save', async function() {
if(!this.isNew) return;
const count = await counter.findByIdAndUpdate(
{_id: 'entityId'},
{$inc: {seq: 1}},
{new: true, upsert: true}
);
// Since count is returning an array
// I used get() to access its child
this.internalId = Number(count.get('seq'))
});
Version: mongoose#5.11.10
None of above answer works when you have unique fields in your schema
because unique check at db level and increment happen before db level validation, so you may skip lots of numbers in auto increments like above solutions
only in post save can find if data already saved on db or return error
schmea.post('save', function(error, doc, next) {
if (error.name === 'MongoError' && error.code === 11000) {
next(new Error('email must be unique'));
} else {
next(error);
}
});
https://stackoverflow.com/a/41479297/10038067
that is why none of above answers are not like atomic operations auto increment in sql like dbs
I use together #cluny85 and #edtech.
But I don't complete finish this issues.
counterModel.findByIdAndUpdate({_id: 'aid'}, {$inc: { seq: 1} }, function(error,counter){
But in function "pre('save...) then response of update counter finish after save document.
So I don't update counter to document.
Please check again all answer.Thank you.
Sorry. I can't add comment. Because I am newbie.
var CounterSchema = Schema({
_id: { type: String, required: true },
seq: { type: Number, default: 0 }
});
var counter = mongoose.model('counter', CounterSchema);
var entitySchema = mongoose.Schema({
testvalue: { type: String }
});
entitySchema.pre('save', function(next) {
if (this.isNew) {
var doc = this;
counter.findByIdAndUpdate({ _id: 'entityId' }, { $inc: { seq: 1 } }, { new: true, upsert: true })
.then(function(count) {
doc.testvalue = count.seq;
next();
})
.catch(function(error) {
throw error;
});
} else {
next();
}
});

Mongoose: updating array in document not working

I'm trying to update an array in document by adding object if it doesn't exist, and replacing the object in array otherwise. But nothing ($push, $addToSet) except the $set parameter does anything, and $set works as expected - overwrites the whole array.
My mongoose schema:
var cartSchema = mongoose.Schema({
mail: String,
items: Array
});
The post request handler:
app.post('/addToCart', function(req, res) {
var request = req.body;
Cart.findOneAndUpdate({
"mail": request.mail
}, {
$addToSet: {
"items": request.item
}
}, {
upsert: true
},
function(err, result) {
console.log(result);
}
);
res.send(true);
});
The data that I'm sending from the client:
{
"mail":"test#gmail.com",
"item":{
"_id":"59da78db7e9e0433280578ec",
"manufacturer":"Schecter",
"referenceNo":"Daemon-412",
"type":"Gitare",
"image":"images/ba9727909d6c3c26412341907e7e12041507489988265.jpeg",
"__v":0,
"subcategories":[
"Elektricne"
]
}
}
EDIT:
I also get this log when I trigger 'addToCart' request:
{ MongoError: The field 'items' must be an array but is of type object in
document {_id: ObjectId('5a19ae2884d236048c8c91e2')}
The comparison in $addToSet would succeeded only if the existing document has the exact same fields and values, and the fields are in the same order. Otherwise the operator will fail.
So in your case, request.item always need to be exactly the same.
I would recommend creating a model of "item". Then, your cart schema would be like:
var cartSchema = mongoose.Schema({
mail: String,
items: [{
type: ObjectId,
ref: 'item',
}],
});
And let MongoDB determine if the item exist.
this should work you just need to implement objectExits function that test if the item is that one you're looking for :
Cart.findOne({ "mail": request.mail })
.exec()
.then(cart => {
var replaced = cart.items.some((item, i) => {
if (item._id == request.item._id)) {
cart.items[i] = request.item;
return true;
}
})
if (!replaced) {
cart.items.push(request.item);
}
cart.save();
return cart;
})
.catch(err => {
console.log(err)
});

Mongoose - MongoDB - Express: Pushing data in a field that is 3 levels deep in a embedded document

What I am trying to achieve or wondering if it is possible is to put in data into my comments field in my "TopicSchema":
var mongoose = require('mongoose');
var TopicSchema = new mongoose.Schema({
username: String,
topic: String,
description: String,
category: String,
created_at: {type: Date, default: Date.now},
posts: [
{
postUsername: String,
post: String,
postUpvote: {type: Number, default: 0},
postDownvote: {type: Number, default: 0},
created_at: {type: Date, default: Date.now},
comments: [
{
commentUsername: String,
comment: String,
created_at: {type: Date, default: Date.now}
}
]
}
]
});
mongoose.model('Topics', TopicSchema);
Here is what I have so far when i'm trying to push into comments in my TopicsSchema
var Topics = mongoose.model('Topics');
var topicUpdate = {
$push: {
"posts:" {
"comments": {
commentUsername: req.body.username,
comment: req.body.comment
}
}
}
}
Topics.update({_id: req.body.topicId}, topicUpdate, function(err, status) {
console.log(status);
if (err) {
console.log('Error adding comment to topic');
res.json(false);
} else {
console.log('Success adding comment to topic');
res.json(true);
}
})
It is inserting here:
var topicUpdate = {
$push: {
"posts:" {
"comments": {
commentUsername: req.body.username,
comment: req.body.comment
}
}
}
}
It is adding a new posts array and not inserting into the correct posts array that I have in my TopicSchema, however it is adding a new field of posts and the comments are also in that posts, which is not what I want to achieve. I wanted the comments to be added to the correct posts field respectively and not add a new posts field with comments in it every time.
I have finally solved it and it seemed to be very challenging with a 3 levels deep embedded document.
My logic was to first find the topic by topic id, then with the data I had received from that, I would have to first loop through the data I had received and if the post id matched with the post id that I was looking for, then I would push the comments to that subarray. After that I would have to use my new data that I had changed and then update the data with the data I had pushed the comments with. The very last thing I had to do was also find the topic again to send to the client side.
Here is the code that I had to finish this with:
Topics.findOne({_id: req.body.topicId}, function(err, data) {
if (err) { res.json(false) }
else {
for (var i = 0; i < data.posts.length; i++) {
if(data.posts[i]._id == req.body.postid) {
data.posts[i].comments.push({comment: req.body.comment, commentUsername: req.body.username }); }
}
Topics.update({_id: req.body.topicId}, data, function(err, status) {
if (err) { console.log('Error updating whole topic'); res.json(false) }
else {
Topics.findOne({_id: req.body.topicId}, function(err, data) {
if (err) {
console.log('Error finding comments');
res.json(status);
} else {
console.log('Success getting comments');
res.json(data);
}
})
}
});
Yes, I know. I learned my lesson to not use embedded documents 3 levels deep with this type of application now. But hey, it works! :) and I hope that I have helped someone out there as well!

mongoose relation between models

I am using mongoose and I have two models: Item and Hashtag.
Hashtag model should contain only name and Item model should contain a list of hashtags (represented by ids).
This is what I've done:
var ItemSchema = new Schema({
hashtags: [ { type: Schema.ObjectId, 'default': null, ref: 'Hashtag' } ],
});
var HashtagSchema = new Schema({
name: { type: String, 'default': '', trim: true },
items: [{ type: Schema.ObjectId, ref: 'Page' }]
});
This is how I try to create an Item:
var item = new Item({
hashtags: ['a', 'b', 'c']
});
item.save(function (err, item) {
if (err) return res.json({ error: err });
res.json(item);
});
Unfortunately I get this error:
CastError: Cast to ObjectId failed for value "a,b,c" at path "hashtags"
How can I solve this?
Since you are using references instead of subdocuments, you need to create the hashtag objects first:
var tagnames = ['a','b','c'];
var hashtags = {}; //for referencing quickly by name later
for (var h in tagnames){
var tag = new Hashtag({
name: tagnames[h]
});
tag.save(function (err, item) {
if (err) console.log('error:',err);
hashtags[item.name] = item;
});
}
Once you have the hashtags created, you can reference them::
var item = new Item({
hashtags: [hashtags.a._id,hashtags.b._id,hashtags.c._id]
});
item.save(function (err, item) {
if (err) return res.json({ error: err });
res.json(item);
});
Then you can use populate to automatically turn the object ids into documents:
Item.find({})
.populate('hashtags')
.exec(function (err, items) {
if (err) return handleError(err);
//items are populated with hashtags
});
If you are just doing simple tagging, then subdocuments may be a better fit. They allow you to declare and save child documents all in one step. The trade-off is that subdocuments belong exclusively to their parent documents. They are not references so any aggregations on them must be done manually.

Categories