Mongoose: use hook/middleware to remove referenced objects (has many) - javascript

In a nodejs app using Mongoose, I have a relation has many: App has many AppClients.
Models:
const mongoose = require('mongoose')
const appSchema = new mongoose.Schema({
name: String,
appClients : [{ type: mongoose.Schema.Types.ObjectId, ref: 'AppClient' }]
})
const App = mongoose.model('App', appSchema)
module.exports = App
const appClientSchema = new mongoose.Schema({
access_key: String,
secret_key: String
})
const AppClient = mongoose.model('AppClient', appClientSchema)
The thing is that I want to remove all AppClients documents related to an App document when it is deleted.
My current code is:
exports.delete = async function(req, res, next) {
const app = await App.findOne({ _id: req.params['id']}).exec()
const listToDelete = [...app.appClients]
await App.deleteOne({ _id: req.params['id']}).exec()
await AppClient.remove({_id: {$in: listToDelete}}).exec()
res.redirect('/apps')
}
This works but I was wondering how to use a hook. I have taken a look at the middleware but I cannot make it work with the pre('remove'), it is never called. I was using something like this:
appSchema.pre('remove', (next) => {
console.log('pre remove') //never called
})

remove is a middleware that's specified on a schema level (like in your example) but it runs on a document level. So the only way to get this fired is to fetch the document and then execute remove() on it
const app = await App.findOne({ _id: req.params['id']}).exec();
await app.remove(); //prints 'pre remove'
There is a paragraph in Mongoose docs about that:
Note: There is no query hook for remove(), only for documents. If you set a 'remove' hook, it will be fired when you call myDoc.remove(), not when you call MyModel.remove(). Note: The create() function fires save() hooks.

According the documentation you should be able to use the hook without finding the document in a first hand.
appSchema.pre('remove', { query: true }, function() {
console.log('remove');
});

Related

Need help figuring out why calling methods from a mongoose schema works fine in one part of my node.js app, but fails elsewhere

I have created the following user schema, including two methods:
getSnapshot()
getLastTweetId()
user.js
const mongoose = require('mongoose')
const getLastTweetId = require('../utilities/getLastTweetId')
const getFollowers = require('../utilities/getFollowers')
const userSchema = new mongoose.Schema({
twitterId: {
type: String,
required: true
},
screenName: {
type: String
},
snapshots: {
type: [snapshotSchema],
default: []
},
createdAt: {
type: Date
},
})
userSchema.method('getSnapshot', async function () {
const { user, snapshot } = await getFollowers({user: this})
await user.save()
return snapshot
})
userSchema.method('getLastTweetId', async function () {
const tweetId = await getLastTweetId({user: this})
return tweetId
})
const User = mongoose.model('User', userSchema)
module.exports = User
When I define a user instance in passport.js, I can call getSnapshot() on user with no problems. (see below)
passport.js
const passport = require('passport')
const mongoose = require('mongoose')
const needle = require('needle')
const { DateTime } = require('luxon')
const User = mongoose.model('User')
// Setup Twitter Strategy
passport.use(new TwitterStrategy({
consumerKey: process.env.TWITTER_CONSUMER_API_KEY,
consumerSecret: process.env.TWITTER_CONSUMER_API_SECRET_KEY,
callbackURL: process.env.CALLBACK_URL,
proxy: trustProxy
},
async (token, tokenSecret, profile, cb) => {
const twitterId = profile.id
const screenName = profile.screen_name
const existingUser = await User.findOne({ twitterId })
if (existingUser) {
// Track if this is a new login from an existing user
if (existingUser.screenName !== screenName) {
existingUser.screenName = screenName
await existingUser.save()
}
// we already have a record with the given profile ID
cb(undefined, existingUser)
} else {
// we don't have a user record with this ID, make a new record
const user = await new User ({
twitterId ,
screenName,
}).save()
**user.getSnapshot()**
cb(undefined, user)
}
}
)
However, when I call getLastTweetId() on a user instance in tweet.js, I receive the following error in my terminal:
TypeError: user.getLastTweetId is not a function
Then my app crashes.
tweets.js
const express = require('express')
const mongoose = require('mongoose')
const User = mongoose.model('User')
const Tweet = mongoose.model('Tweet')
const { DateTime } = require('luxon')
const auth = require('../middleware/auth')
const requestTweets = require('../utilities/requestTweets')
const router = new express.Router()
const getRecentTweets = async (req, res) => {
const twitterId = req.user.twitterId
const user = await User.find({twitterId})
*const sinceId = user.getLastTweetId()*
let params = {
'start_time': `${DateTime.now().plus({ month: -2 }).toISO({ includeOffset: false })}Z`,
'end_time': `${DateTime.now().toISO({ includeOffset: false })}Z`,
'max_results': 100,
'tweet.fields': "created_at,entities"
}
if (sinceId) {
params.since_id = sinceId
}
let options = {
headers: {
'Authorization': `Bearer ${process.env.TWITTER_BEARER_TOKEN}`
}
}
const content = await requestTweets(twitterId, params, options)
const data = content.data
const tweets = data.map((tweet) => (
new Tweet({
twitterId,
tweetId: tweet.id,
text: tweet.text,
})
))
tweets.forEach(async (tweet) => await tweet.save())
}
// Get all tweets of one user either since last retrieved tweet or for specified month
router.get('/tweets/user/recent', auth, getRecentTweets)
module.exports = router
I would really appreciate some support to figure out what is going on here.
Thank you for bearing with me!
My first guess was that the user instance is not created properly in tweets.js, but then I verified via log messages that the user instance is what I expect it to be in both passport.js as well as tweets.js
My second guess was that the problem is that the user instance in the database was created before I added the new method to the schema, but deleting and reinstantiating the entire collection in the db changed nothing.
Next I went about checking if the issue is related to instantiating the schema itself or just importing it and it seems to be the latter, since when I call getLastTweetId in passport.js it also works, when I call getSnapshot() in tweets.js it also fails.
This is where I'm stuck, because as far as I can tell, I am requiring the User model exactly the same way in both files.
Even when I print User.schema.methods in either file, it shows the following:
[0] {
[0] getSnapshot: [AsyncFunction (anonymous)],
[0] getLastTweetId: [AsyncFunction (anonymous)]
[0] }
It looks like my first guess regarding what was wrong was on point, and I was just sloppy in verifying that I'm instantiating the user correctly.
const user = await User.find({twitterId})
The above line was returning an array of users.
Instead, I should have called:
const user = await User.findOne({twitterId})
I did not detect the bug at first, because logging an array that contains only one object looks nearly the same as just logging the object itself, I simply overlooked the square brackets.
Changing that single line fixed it.

Mongoose populate returns empty array or list of ObjectIds

I am practicing my express.js skills by building a relational API and am struggling to populate keys in a schema.
I am building it so I have a list of properties, and those properties have units. The units have a propertyId key.
This is currently returning an empty array, whereas if i remove the populate({}) it returns an array of ObjectIds.
I've read a number of posts and some people solved this by using .populate({path: 'path', model: Model}); but this doesn't seem to be doing the trick. I think it might be the way I am adding a propertyId to the unit but I'm not sure. Can anyone see where I am going wrong? Any help will be massively appreciated.
Here are the schemas.
Property:
const mongoose = require('mongoose');
const { Schema } = mongoose;
const PropertySchema = new Schema({
title: {
type: String,
required: true
},
date: {
type: Date,
default: Date.now
},
units: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'unit'
}
]
});
module.exports = Property = mongoose.model('property', PropertySchema);
Unit:
const mongoose = require('mongoose');
const { Schema } = mongoose;
const UnitSchema = new Schema({
title: {
type: String,
required: true
},
propertyId: {
type: Schema.Types.ObjectId,
ref: 'property'
}
});
module.exports = Unit = mongoose.model('unit', UnitSchema);
I am then creating the unit like this:
-- api/properties/:id/units --
router.post('/:id/units', async (req, res) => {
// Get fields from req.body
const { title } = req.body;
// Get current property
const property = await Property.findById(req.params.id);
try {
// Throw error if no property
if (!property) {
return res.status(400).json({ msg: 'Property not found' });
}
// Create new unit
const newUnit = new Unit({
title,
propertyId: req.params.id
});
// Add new unit to property's units array
property.units.unshift(newUnit);
// Save property
await property.save();
// Return successful response
return res.status(200).json(property);
} catch (error) {
console.error(error.message);
return res.status(500).send('Server error');
}
});
And trying to populate in the GET request
-- /api/properties/:id/units --
const Unit = require('../../models/Unit');
router.get('/:id/units', async (req, res) => {
const property = await Property.findOne({ _id: req.params.id }).populate({path: 'units', model: Unit});
const propertyUnits = property.units;
return res.status(200).json(propertyUnits);
});
If i remove the .populate({path: 'units', model: Unit});, I get a list of unit id's like this:
[
"5ff7256cda2f5bfc1d2b9108",
"5ff72507acf9b6fb89f0fa4e",
"5ff724e41393c7fb5a667dc8",
"5ff721f35c73daf6d0cb5eff",
"5ff721eb5c73daf6d0cb5efe",
"5ff7215332d302f5ffa67413"
]
I don't know, why you don't try it like this:
await Property.findOne({ _id: req.params.id }).populate('units')
I've been try that code above and it's working.
Note: Make sure to check your req.params.id is not null or undefined and make sure the data you find is not empty in your mongodb.
Updated: I've been try your code and it's working fine.
The issue was caused by inconsistent naming and not saving the new created unit as well as the updated property.
I double checked all my schema exports and references and noticed I was using UpperCase in some instances and LowerCase in others, and saved the newUnit as well as the updated property in the POST request and it worked.

Mongoose only saves id and versionKey upon .save

I'm not really sure what happens here, I set up my Schema properly, and the .save function itself works, however it only generates an object with id and versionkey (_id and __v) as properties.
Here's my code for the model (in the file todo.model.js):
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
let Todo = new Schema({
desc: String,
progress: Number
});
module.exports = mongoose.model('Todo', Todo);
And here's the function:
let Todo = require('./todo.model');
...
...
router.route('/add').post(function(req, res) {
let todo = new Todo(req.body);
todo.save()
.then(todo => {
res.status(200).json({'todo': 'todo added successfully'});
})
.catch(err => {
res.status(400).send('adding new todo failed');
});
});
The routing etc. works, and a HTTP Post request with the correct json also returns "todo added successfully", but when i use get, i only get the id and versionKey of the object back.
{
"_id": "5ccd94f4a98874a847e54b00",
"__v": 0
}
What's going wrong here?

model in not defined error in node js

I am writing post api using restify and mongodb with mongoose.
'use strict'
const Trending = require('../models/trending');
const trendingController = {
postTrending: (req, res, next)=>{
let data = req.body || {};
console.log(Trending);
let Trending = new Trending(data);
Trending.save(function(err) {
if (err) {
console.log(err)
return next(new errors.InternalError(err.message))
next()
}
res.send(201)
next()
})
}
}
here error is that Trending is not defined, I don't know why.. other similar controllers are working fine.
Trending is mongoose Model
model code
'use strict'
const mongoose = require('mongoose');
const timestamps = require('mongoose-timestamp');
const Schema = mongoose.Schema;
const TrendingSchema = new mongoose.Schema({
_id: Schema.Types.ObjectId,
headline: {
type: String,
required: true
},
description: String,
data: [
{
heading: String,
list: [String]
}
],
tags: [{ type: Schema.Types.ObjectId, ref: 'Tags' }]
});
TrendingSchema.plugin(timestamps);
const Trending = mongoose.model('Trending', TrendingSchema)
module.exports = Trending;
folder structure
controllers
--- trending.js
models
---trending.js
You are having this problem because of this line;
let Trending = new Trending(data);
You should avoid using the same variable name for two different things to prevent this kind of problem. Especially in this case where you are using uppercase letter for an object when you should use it only for classes.
Replace that line with;
let trending = new Trending(data);
The problem happens because let (and const) are block scoped so a variable with the same name but from an outer scope will be overridden. You then get undefined for this variable because you are referencing it in the same line you are declaring it, so it is in fact still undefined.

How to add save another Schema data to a model?

I am using mongoose with Mongodb v3.4.3
Below is my image model code
const mongoose = require("mongoose");
const CoordinateSchema = require("./coordinate");
const ImageSchema = new mongoose.Schema({
image_filename: {
type: String,
required: true
},
image_url: {
type: String,
required: true
},
coordinates: [CoordinateSchema],
});
Below is my CoordinateSchema code
const mongoose = require("mongoose");
const CoordinateSchema = new mongoose.Schema({
coordinates : {
type: Array,
default: [],
}
});
module.exports = CoordinateSchema;
Below is my api js code running on express,
router.post('/receiveCoordinates.json', (req, res, next) => {
Image.findOneAndUpdate({image_filename:req.body.file_name}).then((image) => {
})
});
How to finish this code so I can store coordinates data in Image model.
Thanks.
UPDATE
To update the coordinates inside of findOneAndUpdate, you simply check that the returned document isn't undefined (which would mean your image wasn't found). Modify your api.js code like so:
router.post('/receiveCoordinates.json', (req, res, next) => {
Image.findOneAndUpdate({image_filename:req.body.file_name}).then((image) => {
if (!image) return Promise.reject(); //Image not found, reject the promise
image.where({_id: parent.children.id(_id)}).update({coordinates: req.body.coordinates}) //Needs to be an array
.then((coords) => {
if (!coords) return Promise.reject();
//If you reach this point, everything went as expected
});
}).catch(() => {
console.log('Error occurred');
);
});
Here's my guess why it isn't working.
In ImageSchema, you are sub-nesting an array of CoordinateSchema. But CoordinateSchema is a document which already contains an array.
This is probably not what you're looking for. If you're using mongoose version 4.2.0 or higher, you can nest CoordinateSchema inside of ImageSchema as a single document. Re-write your ImageSchema like this:
// ...
const ImageSchema = new mongoose.Schema({
// ...
coordinates: CoordinateSchema,
});
If this didn't work or doesn't resolve your issue, please let me know so we can work together to find a solution.

Categories

Resources