Mongoose Connection with MongoDB - node.js

I've been working with Mongoose/MongoDB and I realized I don't quite understand how the connection between mongoose and my MongoDB database persists. Let's say I have Node application with a server.js file.
const express = require('express')
const dotenv = require('dotenv')
dotenv.config()
const connectDB = require('./config/db')
connectDB()
const app = express()
app.get('/', (req, res) => {
res.send('API is running...')
})
const PORT = process.env.PORT || 5000
app.listen(
PORT,
console.log(`Server running in ${process.env.NODE_ENV} port ${PORT}`)
)
And then I have my database configuration file where the connection is initiated.
const mongoose = require('mongoose')
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGO_URI, {
useUnifiedTopology: true,
useNewUrlParser: true,
useCreateIndex: true,
})
console.log(`MongoDB Connected: ${conn.connection.host}`)
} catch (error) {
console.error(`Error: ${error.message}`)
process.exit(1)
}
}
module.exports = connectDB
My question is, after the database is connected with mongoose.connect(), how does it persist throughout the rest of the application? Obviously, this allows me to interact with the database in other files, but I'm not quite clear on what's going on "underneath the hood". Let's say I have a Product model
const mongoose = require('mongoose')
const reviewSchema = mongoose.Schema(
{
name: { type: String, required: true },
rating: { type: Number, required: true },
comment: { type: String, required: true },
},
{
timestamps: true,
}
)
const productSchema = mongoose.Schema(
{
user: { type: mongoose.Schema.Types.ObjectId, required: true, ref: 'User' },
name: { type: String, required: true },
image: { type: String, required: true },
brand: { type: String, required: true },
category: { type: String, required: true },
description: { type: String, required: true },
reviews: [reviewSchema],
rating: { type: Number, required: true, default: 0 },
numReviews: { type: Number, required: true, default: 0 },
price: { type: Number, required: true, default: 0 },
countInStock: { type: Number, required: true, default: 0 },
},
{
timestamps: true,
}
)
const Product = mongoose.model('Product', productSchema)
module.exports = Product
I can then have a route
router.get(
'/',
asyncHandler(async (req, res) => {
const products = await Product.find({})
res.json(products)
})
)
Which would operate on this Product model. My question is, how is mongoose aware of the database still (after the initial connection) that allows it to call methods on this Model that allow it to interact with the database.

I'm not an expert in MongoDB nor JavaScript but I think that you might have misunderstood some concepts.
Mongoose as it says on its website provides a schema-based solution module to your application data.
You use mongoose to connect with the database through its methods, the database is not connected with mongoose.
Dotenv file basically stores your server configuration data within your project
separating it from the code that allows you to display port info, processes etc in a clear way.
Let's start reviewing the first file
Here you create some const of express and dotenv
const express = require('express')
const dotenv = require('dotenv')
After that, you apply the config method to the dotenv const (you can also add that method at the first line)
dotenv.config()
Now you create the connectDB const loading the content from the DB file (which I suppose it's the next one)
const connectDB = require('./config/db')
Calling the method of the class starts the connection with the Database:
connectDB()
Now you use express to make a get request that will be displayed on your root folder and basically will display some basic data from the port and the process (loaded from ENV file)
const app = express()
app.get('/', (req, res) => {
res.send('API is running...')
})
const PORT = process.env.PORT || 5000
app.listen(
PORT,
console.log(`Server running in ${process.env.NODE_ENV} port ${PORT}`)
)
Let's review now the other file.
You create a const from mongoose
const mongoose = require('mongoose')
And establish a connection to your DB
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGO_URI, {
useUnifiedTopology: true,
useNewUrlParser: true,
useCreateIndex: true,
})
console.log(`MongoDB Connected: ${conn.connection.host}`)
} catch (error) {
console.error(`Error: ${error.message}`)
process.exit(1)
}
}
This is the important part, you export that const to the whole project so it can be accessed from different parts of the project that's the reason that allows you to connect with the DB without providing the same credentials every single time, and the same logic applies to dotenv file
module.exports = connectDB
((If you're not familiar with Java just ignore this:))
I was working today with MySQL and Java JDBC API and I created some files that allow me to interact with the DB like you're doing with Mongoose, see:
//variables to get DB info and connection
private Connection connection;
private PreparedStatement stmt = null;
private ResultSet rs = null;
private Statement statement = null;
//Constructor, when you create an object of this class will establish a connection
with the dbCredentials variables (like dotenv)
public DevOps() throws SQLException {
this.connection = DriverManager.getConnection(dbCredentials.getDbname(), dbCredentials.getUsername(), dbCredentials.getPass());
}
This would be like your dotenv file, you just have to change the data here instead of changing lots of lines of code when you migrate your DB or update credentials
public class dbCredentials {
public final static String username = "user";
public final static String pass = "password";
public static String dbname = "jdbc:mysql://192.168.56.1/school";
public static String allowMultiQueries() {
return dbname+"?allowMultiQueries=true";
}
public static String getUsername() {
return username;
}
public static String getPass() {
return pass;
}
public static String getDbname() {
return dbname;
}
}
And you basically just have to do this:
DevOps devops = new DevOps();
which would be similar to:
const connectDB = require('./config/db')
So basically the concept of all of this is to keep your code organized, ofc you can have all of that on a single file but in terms of scalability would be painful to maintain especially when you have to apply any change to your DB.
Hope this was useful for you, just ask me if you have any doubt!

Related

MongoDB Atlas: Cannot GET from API

I developed a simple API that is supposed to GET data from MongoDB Atlas. Unfortunately, if I directly hit the browser or use Postman, I am getting an error:
Cannot GET /api/rooms/getallrooms
server.js
const express = require('express');
const app = express();
const dbConfig = require('./db');
const roomsRoute = require('./routes/roomsRoute');
app.use('api/rooms', roomsRoute);
const port = process.env.PORT || 5001;
app.listen(port, () => {
console.log(`Server started on port ${port} :)`);
});
roomsRoute.js:
const express = require('express');
const router = express.Router();
const Room = require('../models/rooms');
router.get('/getallrooms', async (req, res) => {
try {
const rooms = await Room.find({});
return res.json({rooms});
} catch (error) {
return res.status(400).json({message: error});
}
});
module.exports = router;
rooms.js (Contains room schema):
const mongoose = require('mongoose');
const roomSchema = mongoose.Schema({
name : {
type: 'String',
required: true,
},
capacity : {
type: 'number',
required: true,
},
contact : {
type: 'number',
required: false,
},
type : {
type: 'String',
required: true,
},
imageURLs : [],
currentBookings : [],
description : {
type: 'String',
required: false,
},
rentPerDay : {
type: 'number',
required: true,
}
},{
timestamps: true,
});
const roomModel = mongoose.model('rooms', roomSchema);
module.exports = roomModel;
My Atlas looks like this:
I have ensured from Network Access that all IP connections are allowed (0.0.0.0/0).
This is my first API so I reckon I'm missing something basic. Can anybody help me figure this out?
Update 1:
By adding "/" in app.use, the error is gone but I'm getting empty response from the DB:
{
"rooms": []
}
Update 2:
After ensuring column names in model and DB are synchronised, still the response I'm getting is empty.
Here:
app.use('api/rooms', roomsRoute);
I think you need to prefix the path with a / like so:
app.use('/api/rooms', roomsRoute);
See the Express documentation for path examples.
The problem is with database URL. By default MongoDB gives the URL of "test" database. Updating the database name fixed the problem for me.

Mongoose schema virtual attribute always returns undefined

I was trying to use mongoose schema virtual functions to implement flags, But I could not make them work The mongoose database is hosted on mongodb altas. I have tried deleting the whole collection and staring again.
Here is a simplified example:
Let us say I have a basic User Schema:
const mongoose = require('mongoose')
const UserSchema = new mongoose.Schema({
name: String,
email: String
}, {toObject: {virtuals: true, getters: true}})
UserSchema.virtual('status').get(() => {
return this.name
})
const User = mongoose.model('User', UserSchema)
module.exports = {
User,
UserSchema
}
In app.js I have the following:
const mongoose = require("mongoose");
const express = require("express");
const {User} = require("./models/User")
const app = express();
app.use(express.urlencoded({extended: true}));
app.use(express.json());
mongoose.connect(process.env.DB_URL, {
useNewUrlParser: true,
useUnifiedTopology: true
});
User.findById("600ae8001931ad49eae40c03", (err, doc) => {
console.log(doc.status)
})
const port = process.env.PORT || 5000;
app.listen(port, () => {
console.log(`server at http://localhost:${port}`);
});
I made sure the id exists, but the result is always undefined. What am I doing wrong here?
based on mongoose documentation
Do not declare methods using ES6 arrow functions (=>). Arrow functions
explicitly prevent binding this, so your method will not have access
to the document and the above examples will not work
so you can not use arrow function in get, do like this :
UserSchema.virtual('status').get(function() {
return this.name
})
It's because of how you are declaring your function being passed to get. The way you are doing it is changing the this reference.
UserSchema.virtual('status').get(function() {
return this.name
})
The above would maintain it
To future users peaple that looking for the answer, beside this answer - if it's not solve the problem and still get undefined -
the problem maybe because you didn't selec the attribute that used in the virtual middleware
Our Schema
const monthSchema = new mongoose.Schema(
{
name: String,
numOfDays: Number,
},
{ toJSON: { virtuals: true }, toObject: { virtuals: true } }
);
if you use numOfDays in virtual middleware to calculate the weeks like this:
MonthSchema.virtual('NumOfWeeks').get(function() {
return this.numOfDays / 7
})
this you must be sure that numOfDays are selected
const user = await User.find().select('name'); // this.numOfDays will be undifined
const user = await User.find().select('-numOfDays'); // this.numOfDays will be undifined
const user = await User.find().select('name numOfDays'); // this.numOfDays will be not undifined
I spent whole day to figure this, may be this will help someone

How does mongoose know what collection I am accessing?

I'm having a trouble grasping a concept in Mongoose.
I'm using MongoDB atlas, got a cluster , a database and 2 collections.
users, characters.
Through a guide I've learned that a good way to write your stuff is to have a model (I use the naming schema) as a file, importing it into your Database module/class
and using it there to perform a query...
const mongoose = require("mongoose");
const process = require("./config.env");
db = () => {
return mongoose
.connect(process.env.URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
useFindAndModify: true,
})
.then((response) => {
console.log(`Connected to Databse : ${response.connection.host}`);
})
.catch((err) => {
console.log("DB_ERROR:", err);
process.exit(1);
});
};
module.exports = db;
const mongoose = require("mongoose");
const UserSchema = new mongoose.Schema({
username: {
type: String,
required: true,
},
password: {
type: String,
required: true,
},
email: {
type: String,
required: true,
},
});
const User = mongoose.model("User", UserSchema);
module.exports = User;
const User = require("../schemas/User");
const db = require("../config/db");
class Database {
constructor(db, collection) {
this.db = db;
this.collection = collection;
this.User = User;
}
connect() {
return db();
}
}
module.exports = Database;
one file to handle the db connection..another file as the User schema and a third file to handle every function i might use globally...
One thing I cannot wrap my mind around is
how is the findOne() function able to locate the collection I am using without me telling it what collection i want it to search in?
is it somehow translating the
const User = mongoose.model("User", UserSchema);
line and searching for "users" as well? I just can't understand the magic behind this...
what if I want to search specifically in the characters collection...?
Mongoose uses the model name, as passed when it was created: mongoose.model("User", UserSchema), converted to lower case and with an 's' appended.
For the model User it uses the collection users by default. You can change this by explicitly specifying the collection name in the schema.

"Mongo Error : Authentication failed" can't read datas from MongoDB Atlas

Today I wanted to use clusters in MongoDB Atlas to get an online DB, instead of my MongoDB local database (which worked perfectly fine),
So, I followed a mLab tutorial,
It works perfectly on writing in the database (when I auth in my website, it adds the datas in the database, when I write a message in the chat it adds the message, etc...)
But when I want to read these datas, I got :
MongoTimeoutError: Server selection timed out after 30000 ms
MongoError: Authentication failed
The connect in my server/index.js seems to work, because I got the console log :
mongoose
.connect(
`mongodb+srv://${process.env.USER}:${process.env.PASSWORD}#ofilms-demo-f9iwz.mongodb.net/test?retryWrites=true&w=majority`,
{ useNewUrlParser: true, useUnifiedTopology: true }
).then(() =>
console.log(
"working"
)
)
.catch(err => console.log(err));
but not other routes, like this one (which get all users in the database) :
const mongo = require("mongodb").MongoClient;
router.get("/getAll", function(req, res) {
console.log("get all users");
const client = new mongo(`mongodb+srv://${process.env.USER}:${process.env.PASSWORD}#ofilms-demo-f9iwz.mongodb.net/test?retryWrites=true&w=majority`, {
useNewUrlParser: true,
useUnifiedTopology: true
});
client.connect(err => {
const collection = client.db("test").collection("users");
collection.find().toArray((err, items) => {
res.json(items);
});
client.close();
});
});
One model from Mongoose :
/* eslint-disable no-undef */
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const UserSchema = new Schema({
username: {
type: String,
required: true
},
email: {
type: String,
required: true,
lowercase: true
},
password: {
type: String,
required: true
},
firstname: String,
lastname: String,
sexe: String,
mobilePhone: String,
departement: Number,
city: String,
moviesLiked: Array,
seriesLiked: Array,
moviesDisliked: Array,
seriesDisliked: Array,
moviesFavorites: Array,
seriesFavorites: Array,
lists: Array,
creationDate: {
type: Date,
default: Date.now
},
lastConnection: Date,
isVerified: Boolean,
isAdmin: Boolean,
isModerator: Boolean,
isConnected: Boolean
});
module.exports = User = mongoose.model("users", UserSchema);
I can show you the code of other files if needed, or give you the link of the repo if someone wants it, but it's a really big project,
Thanks,
You seem to connect mongodb with both mongoose.connect() and MongoClient.connect(), one of them will be enough.
If you want to use mongoose you can connect to mongodb in your main file (index.js or app.js), and when connected to the db your server can start listening.
And you don't need to connect mongodb in your routes.
For example:
index.js (main file)
const express = require("express");
const app = express();
require("dotenv").config();
const users = require("../routes/users"); //todo: correct the users route path
app.use("/api/users", users);
mongoose
.connect(
`mongodb+srv://${process.env.USER}:${process.env.PASSWORD}#ofilms-demo-f9iwz.mongodb.net/test?retryWrites=true&w=majority`,
{ useNewUrlParser: true, useUnifiedTopology: true }
)
.then(() => console.log("working"))
.catch(err => console.log(err));
in your route: (users.js)
const express = require("express");
const router = express.Router();
const User = require("../../models/User");
router.get("/users", async (req, res) => {
const users = await User.find({});
res.send(users);
});
module.exports = router;
As you see there is no connection related code in our route, because we have already connected when the application starts.
For this code to work, you need to add your local IP to the IP whitelist IP in mongodb atlas panel. (SECURITY --> Network Access --> IP Whitelist.
Also the user you are using to connect must have read an write priveleges.
You can check your users priveleges in SECURITY --> Database Access --> MongoDB Users.

Mongoose - multiple database connections

I want to understand how to switch between databases within mongoose global promise connection.
My current connection is established this way app.ts
import * as mongoose from 'mongoose';
...
try {
await mongoose.createConnection(`mongodb://localhost:27017/db1`, {
useNewUrlParser: true,
})
console.log("Connected")
} catch (error) {
console.log(error)
}
And then I am accessing it in different files some.model.ts
import { Schema, Document, model } from 'mongoose';
const SomeSchema: Schema = new Schema({
name: { type: String, required: true },
owner: { type: string, required: true }
});
export default model('Some', SomeSchema);
According to documentation.
So far we've seen how to connect to MongoDB using Mongoose's default connection. At times we may need multiple connections open to Mongo, each with different read/write settings, or maybe just to different databases for example. In these cases we can utilize mongoose.createConnection() which accepts all the arguments already discussed and returns a fresh connection for you.
const conn = mongoose.createConnection('mongodb://[username:password#]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]', options);
I can create multiple database connections like this
try {
const db1 = await mongoose.createConnection(`mongodb://localhost:27017/db1`, {
useNewUrlParser: true,
})
const db2 = await mongoose.createConnection(`mongodb://localhost:27017/db2`, {
useNewUrlParser: true,
})
console.log("Connected")
} catch (error) {
console.log(error)
}
I can see both connection in console.log(mongoose.connections)
But how can I specify what database should be used for the Model in some.model.ts?
import { Schema, Document, model } from 'mongoose';
const SomeSchema: Schema = new Schema({
name: { type: String, required: true },
owner: { type: string, required: true }
});
export default SPECIFY_DATABASE.model('Some', SomeSchema);
I have found other questions like this but there are connections created "localy", I need to use mongoose connection across many different files.
Thank you for answers, if you need more explanation please let me now.
You need to actually return the connection, and then register a given model to each of the connections. To clarify, you need:
something to create a (named, specific) connection
schemas
you create models by registering schemas to the given connections,
you also need something to orchestrate it.
Example, lets have a "db.js" file (I call mine "repo.js" usually) with a single export, a function that returns the initialized database Promise.
You'd use it by importing the function and awaiting for the db.
I have a bit of a longer example, so error handling etc is ommited for brevity.
import { createConnections } from './create-connections';
import { UsersSchema } from './users-schema';
import { PostsSchema } from './posts-schema';
let db: any;
export function getDatabase(): Promise<any> {
if (this.db) return Promise.resolve(db);
return createDatabases();
}
async function createDatabases() {
const { db1, db2 } = await createConnections('mongodb://localhost/db1', 'mongodb://localhost/db2');
const UserModel = db1.model('users', UserSchema);
const PostModel = db2.model('posts', PostSchema);
db = {
UserModel,
PostModel,
// also if you need this
connections: {
db1,
db2,
}
}
return db;
}
Now, I've used './create-connections' here, which is almost what you have:
// create-connection.js
const { createConnection } = require('mongoose');
// You create connections by calling this function and giving it the URL to the server
export function createConnections(url1, url2) {
const db1 = await createConnection(url1);
const db2 = await createConnection(url2);
return {
db1,
db2
}
}
Now, let's say you have two models: users and posts, let's have their schemas.
// users schema
import { Schema, Document } from 'mongoose';
export const UserSchema: Schema = new Schema({
name: { type: String, required: true },
});
// posts schema
import { Schema, Document } from 'mongoose';
export const PostSchema: Schema = new Schema({
text: { type: String, required: true },
owner: { type: SchemaID, required: true }
});
So now you need to bind it all in that fdirst file.
But how to use it? As I've said, since it's async, you always import it and use it as a simple async getDB:
// some controller, route handler, service etc.
import { getDatabase } from './get-database';
router.get('/users', async (req, res) => {
const User = await getDatabase().UserModel;
const users = await User.find();
return res.json(users);
});
router.post('/posts', async (req, res) {
const { text } = req.body;
const owner = req.user.id;
const Post = await getDatabase().PostModel;
const post = await Post.create({ text, owner });
return res.json(post);
});

Resources