The correct way to create collection during mongoose transaction - mongodb

How to autocreate collection during mongoose transaction if the collection was not created yet?
I'm aware of mongoose limitation that restricts user to create (or delete) mongoose collections during open transaction session.
Also, I was able to find 3 possible solutions on how to fix that:
1. autoCreate option
2. Model.init() method
3. Model.createCollection() method
Which one to use? Without losing indexes etc.
app.models.ts
import { model, Schema } from 'mongoose';
const UserSchema = new Schema<UserDocument>({
name: {
type: Schema.Types.String,
required: true,
}
}); // { autoCreate: true } <-- ???
export const UserModel = model<UserDocument>('User', UserSchema);
app.ts
import { startSession } from 'mongoose';
import { UserModel } from './app.models.ts';
async function createUser() {
// await UserModel.createCollection(); ??
// or
// await UserModel.init(); ??
const session = await startSession();
sesssion.startTransaction();
try {
const [user] = await UserModel.create([{ name: 'John' }], { session });
await session.commitTransaction();
return user;
} catch (error) {
await session.abortTransaction();
} finally {
session.endSession()
}
}
foo();

If a collection does not exist, MongoDB creates the collection when you first store data for that collection. You can also explicitly create a collection with various options, such as setting the maximum size or the documentation validation rules.
Anyway, mongoose takes care of indexes, collection, etc...
you just need to define the collection name: https://mongoosejs.com/docs/guide.html#collection
const UserSchema = new Schema<UserDocument>({
name: {
type: Schema.Types.String,
required: true,
}
}, {collection: 'users'});
There is the answer about transactions and collection creating -https://github.com/Automattic/mongoose/issues/6699
Actually, I use https://www.npmjs.com/package/db-migrate package to create collections and indexes before starting an app.

Related

How to populate an array of ObjectIds in mongoose?

I have a User model with a schema that I would like to validate an array of multiple friends by their id's. The portion of the schema that is supposed to do this is:
friends: {
type: [mongoose.SchemaTypes.ObjectId],
},
Then, when I try to add a friend with an id value and populate it inside the API endpoint, it adds the id to the database, but does not populate it. Here is the code:
if (method === "POST") {
const userId = getIdFromCookie(req);
try {
const newFriend = {
friends: req.body.friend
};
const updatedUser = await User.findByIdAndUpdate(userId, newFriend, {new: true})
const popUser = await User.findById(userId).populate("friends")
res.status(200).json({success: true, data: updatedUser});
} catch (error) {
res.status(400).json({success: false});
}
} else {
res.status(400).json({error: "This endpoint only supports method 'POST'"})
}
I want to know how I can add a friend's id to the database, whilst simultaneously populating it in the same endpoint.
The user schema is missing the ref field.
Example from the docs:
stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]
Without the ref, Mongoose doesn't know where to lookup the ObjectId.

Opening Mongoose connection in AdonisJS provider times out

I was following this article to use Mongo in AdonisJS 5 project.
I have an AdonisJS provider which I have created by node ace make:provider Mongo (it is registered in .adonisrc.json):
import { ApplicationContract } from '#ioc:Adonis/Core/Application'
import { Mongoose } from 'mongoose'
export default class MongoProvider {
constructor(protected app: ApplicationContract) {}
public async register() {
// Register your own bindings
const mongoose = new Mongoose()
// Connect the instance to DB
await mongoose.connect('mongodb://docker_mongo:27017/mydb')
// Attach it to IOC container as singleton
this.app.container.singleton('Mongoose', () => mongoose)
}
public async boot() {
// All bindings are ready, feel free to use them
}
public async ready() {
// App is ready
}
public async shutdown() {
// Cleanup, since app is going down
// Going to take the Mongoose singleton from container
// and call disconnect() on it
// which tells Mongoose to gracefully disconnect from MongoBD server
await this.app.container.use('Mongoose').disconnect()
}
}
My model is:
import { Schema, model } from '#ioc:Mongoose'
// Document interface
interface User {
email: string
}
// Schema
export default model(
'User',
new Schema<User>({
email: String,
})
)
Controller:
import { HttpContextContract } from '#ioc:Adonis/Core/HttpContext'
import User from 'App/Models/User'
export default class UsersController {
public async index({}: HttpContextContract) {
// Create a cat with random name
const cat = new User({
email: Math.random().toString(36).substring(7),
})
// Save cat to DB
await cat.save()
// Return list of all saved cats
const cats = await User.find()
// Return all the cats (including the new one)
return cats
}
}
And it is timeouting.
It is working, when I open the connection in controller like this though:
import { HttpContextContract } from '#ioc:Adonis/Core/HttpContext'
import User from 'App/Models/User'
import mongoose from 'mongoose'
export default class UsersController {
public async index({}: HttpContextContract) {
await mongoose.connect('mongodb://docker_mongo:27017/mydb')
// Create a cat with random name
const cat = new User({
email: Math.random().toString(36).substring(7),
})
// Save cat to DB
await cat.save()
// Return list of all saved cats
const cats = await User.find()
// Close the connection
await mongoose.connection.close()
// Return all the cats (including the new one)
return cats
}
}
I have just created an AdonisJS provider, registered it in .adonisrc.json, created a contracts/Mongoose.ts with typings, and use the model in controller.
Any idea? I'm stuck for a day with this.
Thanks
I managed to resolve this issue by not storing mongoose in a variable. It seems the mongoose variable you declare in your MongoProvider is the root of your timeout error.
So I did as follow :
export default class MongoProvider {
constructor(protected app: ApplicationContract) {}
public async register() {
await mongoose.connect('mongodb://localhost:27017/dbName')
this.app.container.singleton('Mongoose', () => mongoose)
}
public async boot() {
// All bindings are ready, feel free to use them
}
public async ready() {
// App is ready
}
public async shutdown() {
await this.app.container.use('Mongoose').disconnect()
}
}
If someone would be interested:
with the help of the article author the reason why it is not working was missing Mongoose when creating the model (Mongoose.model instead of just model:
export default Mongoose.model(
'User',
new Schema<User>({
email: String,
})
)
I followed this article too, and I have the same issue you discussed. but resolved this by importing mongoose in my model a little differently.
import mongoose in the model like this import Mongoose, { Schema } from '#ioc:Mongoose' instead of import { Schema, model } from '#ioc:Mongoose'
Example:
import Mongoose, { Schema } from '#ioc:Mongoose'
// Document interface
interface User {
email: string
}
// Schema
export default model(
'User',
new Schema<User>({
email: String,
})
)

Type 'string' is not assignable to type 'Condition<UserObj>' when making mongoose query by ID

I have the following API route in Next:
import {NextApiRequest, NextApiResponse} from "next";
import dbConnect from "../../utils/dbConnect";
import {UserModel} from "../../models/user";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "GET") return res.status(405);
if (!req.query.id || Array.isArray(req.query.id)) return res.status(406).json({message: "No ID found in request"});
try {
await dbConnect();
const user = await UserModel.findOne({ _id: req.query.id });
if (!user) return res.status(404).json({message: "No user found"});
return res.status(200).json({data: user});
} catch (e) {
return res.status(500).json({message: e});
}
}
Typescript says that the line const user = await UserModel.findOne({ _id: req.query.id }); contains error Type 'string' is not assignable to type 'Condition<UserObj>'. Creating an ObjectId instead of a string (const user = await UserModel.findOne({ _id: mongoose.Types.ObjectId(req.query.id) });) throws the same error.
I've looked through the type files/docs but I'm struggling to figure out why this is invalid. Shouldn't querying by ID with a string or ObjectId be a valid condition object? Querying by other fields works fine.
Why is this invalid, and how should I fix it?
The proposed solution by #Tim is good and solves this punctual situation, but it doesn't get you to the root of the problem. What if you have to use the findOne method because you are going to use another field in the filter? For example:
You want to get the user with that id and that the deletedAt attribute is null.
const user = await UserModel.findOne({ _id: req.query.id, deletedAt: null});
You will get the same error cause the mistake is in the userModel definition. I guess your user class is basically as shown below:
import { ObjectId, Types } from 'mongoose';
#Schema({ versionKey: false, timestamps: true })
export class User {
#Field(() => ID, {name: 'id'})
readonly _id: ObjectId;
#Field(() => Date, {nullable: true, name: 'deleted_at'})
#Prop({type: Date, required: false, default: null})
deletedAt?: Date;
#Field()
#Prop({required: true, index: true})
name: string;
...
}
The problem is that you are directly accessing the Schema user when you should be accessing the model (repository pattern).
[SOLUTION]: Create the model or the repository for your user class, and use it to interact with your database.
In my case I just added the following lines:
import { ObjectId, Types, Document } from 'mongoose';
#Schema({ versionKey: false, timestamps: true })
export class User {
...
}
export type UserDocument = User & Document;
OR
import { ObjectId, Types, Document } from 'mongoose';
#Schema({ versionKey: false, timestamps: true })
export class User extends Document{
...
}
and in my service I instantiated an object of type model:
import { Model } from 'mongoose';
private userModel: Model<UserDocument>;
and then I was able to make the following method call:
...
await dbConnect();
const user = await UserModel.findOne({ _id: req.query.id });
if (!user) return res.status(404).json({message: "No user found"});
...
Use .findByID for id based queries.

Google Calendar API (Saving events in MongoDB, Express JS)

I can't figure out how to save fetched events from Calendar API. I was able to print out array of events in console. I would require save multiple events at once and have verification if they already exist in database with unique id.
Here's my event.js scheme in express js.
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const EventSchema = new Schema({
id: {
type: String,
required: false,
unique:true
},
start: {
type: String
},
end: {
type: String
},
status: {
type: String
},
creator: {
type: Array
},
description: {
type: String
}
});
module.exports = Event = mongoose.model('events', EventSchema);
Here's my event.js router in express js.
router.post("/google/get", async (req, res, next) => {
const {
google
} = require('googleapis')
const {
addWeeks
} = require('date-fns')
const {
OAuth2
} = google.auth
const oAuth2Client = new OAuth2(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET
)
oAuth2Client.setCredentials({
refresh_token: process.env.GOOGLE_REFRESH_TOKEN,
})
const calendar = google.calendar({
version: 'v3',
auth: oAuth2Client
})
calendar.events.list({
calendarId: 'MY CALENDAR ID',
timeMin: new Date().toISOString(),
timeMax: addWeeks(new Date(), 1).toISOString(),
singleEvents: true,
orderBy: 'startTime',
},
function (err, response) {
if (err) {
console.log("The API returned an error: " + err)
return
}
var events = response.data.items
events.forEach(function (event) {
var start = event.start.dateTime || event.start.date
console.log("%s - %s", start, event.summary)
})
}
)
In Mongoose, in order to save something to a database, all you need to do is to instantiate the model that you created. Your event schema exports Event as a model that you can then treat as a regular object. So you would do something along the lines of:
let currentEvent = new Event({id, start, end, status, creator, description});
currentEvent.save();
Once that is done, it should be stored in your MongoDB. I assume that as the code for this is not visible it is already set up and working. You can just run the above inside of your for loop with some minor tweaks to grab each value correctly and it should sort your issue out!
As for your unique ID and making sure that it doesn't already exist in your database, you can use the same model to find values by checking the id against your database and seeing if it exists. As follows:
Event.findById(id, (err, event) => {
if(event == null) {
let currentEvent = new Event({id, start, end, status, creator, description});
currentEvent.save();
} else {
alert("Error, this event already exists")
}
});
I believe something like this should work, however I might have it wrong with how to check if the event exists, I can't remember if it returns null or something different, so just console log the value of event and check to see what it returns if there isn't an event that exists with that ID, and just re-run your if statement with that instead.

How to query nested data in mongoose model

I am attempting to build a Vue.js app with a MEVN stack backend and Vuex. I am configuring my Vuex action handler with a GET request that prompts a corresponding Express GET route to query data nested in Mongoose.
A username is passed into the handler as an argument and appended to the GET request URL as a parameter:
actions: {
loadPosts: async (context, username) => {
console.log(username)
let uri = `http://localhost:4000/posts/currentuser?username=${username}`;
const response = await axios.get(uri)
context.commit('setPosts', response.data)
}
}
The corresponding Express route queries activeUser.name, which represents the nested data in the Mongoose Model:
postRoutes.route('/currentuser').get(function (req, res) {
let params = {},
username = req.query.activeUser.name
if (username) {
params.username = username
}
Post.find(params, function(err, posts){
if(err){
res.json(err);
}
else {
res.json(posts);
}
});
});
Below is my Mongoose model, with activeUser.name representing the nested data queried by the Express route:
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
let Post = new Schema({
title: {
type: String
},
body: {
type: String,
},
activeUser: {
name: {
type: String
}
}
},{
collection: 'posts'
});
module.exports = mongoose.model('Post', Post);
Even with this setup, the GET route does not appear to send a response back to the action handler. I thought adding username = req.query.activeUser.name in the express route would be the right method for querying the nested data in Mongoose, but apparently not. Any recommendations on how to configure the above Express route in order to query the nested data in the Mongoose model? Thanks!
name is inside activeuser so you need to construct params object variable like this:
postRoutes.route("/currentuser").get(function(req, res) {
let params = {
activeUser: {}
};
let username = req.query.activeUserName;
if (username) {
params.activeUser.name = username;
}
Post.find(params, function(err, posts) {
if (err) {
res.json(err);
} else {
res.json(posts);
}
});
});
Note that I also used activeUserName as query param like this: /currentuser?activeUserName=JS_is_awesome18