Querying in-memory mongodb with Jest - node.js

I am trying to unit test my code, I am using Node.js, Express, MongoDB, Mongoose and trying to test it with Jest.
I am having issues with the in-memory mongo db with Jest. I have read the documentation (https://jestjs.io/docs/en/mongodb).
When trying to test a query for one of my Mongoose models, I believe that my query is not pointing at the correct place, I would like it to run its query against the in-memory collection.
My current test setup:
const { MongoClient } = require("mongodb");
const Preferences = require("./preferences");
const mongoose = require("mongoose");
const ObjectId = mongoose.Types.ObjectId;
describe("Preference helper function", () => {
let connection;
let db;
let userPreferences;
const testUser = new Preferences({
userId: ObjectId("5eef15429e93464b3ccae235"),
wordPreferences: [
{ wordId: 1, active: true},
{ wordId: 2, active: true},
{ wordId: 3, active: true},
{ wordId: 4, active: true},
{ wordId: 5, active: true}
],
});
beforeAll(async () => {
connection = await MongoClient.connect(global.__MONGO_URI__, {
useNewUrlParser: true,
});
db = await connection.db(global.__MONGO_DB_NAME__);
});
afterAll(async () => {
await connection.close();
await db.close();
});
beforeEach(async () => {
userPreferences = await db.collection("userPreferences");
});
afterEach(async () => {
await userPreferences.deleteMany();
});
it("get active words function gives correct result", async () => {
await userPreferences.insertOne(testUser);
const expectedResult = [1, 2, 3, 4, 5];
let queryResult = await Preferences.getActiveWordIds(testUser.userId);
expect(queryResult).toEqual(expectedResult);
});
});
Running this code gives the following error:
Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error
Changing the timeout for this does not resolve the problem as it appears the query does not resolve. Here is the the Moongoose model for the Preference model used in the test above:
const mongoose = require("mongoose");
const PreferenceMappingSchema = require("../preference-mapping");
var PreferenceSchema = new mongoose.Schema({
userId: {
type: mongoose.Types.ObjectId,
required: true,
},
wordPreferences: {
type: [PreferenceMappingSchema],
required: true,
},
});
// Choose the correct db and set the model
const db = mongoose.connection.useDb("users");
const Preferences = (module.exports = db.model("Preference", PreferenceSchema));
module.exports.getActiveWordIds = async function(userId) {
try {
const user = await Preferences.aggregate([
// Get the current users doc
{ $match: { userId: userId} },
{
$project: {
wordPreferences: {
// Get only active words
$filter: {
input: "$wordPreferences",
as: "pref",
cond: {
$eq: ["$$pref.active", true],
},
},
},
},
}
]).exec();
// Map to list of valid numbers
return user[0]['wordPreferences'].map(x => x.wordId);
} catch (error) {
console.warn(error);
throw error;
}
};
Thanks in advance for any help!

Related

Mongoose and express insert many many-to-many relationships in one request

I want to make a request in which I add several meals to order and a request that removes several meals from order. It is many-to-many relationship because order can have several meals and meal can have several orders.
This is my order model:
const mongoose = require("mongoose");
const orderObject = {
cook: { type: mongoose.Types.ObjectId, ref: "CookRoleUser" },
remark: { type: String },
delivery_address: { type: String, required: true },
orderer: {
type: mongoose.Types.ObjectId,
ref: "RegularRoleUser",
required: true,
},
meals: [{ type: mongoose.Types.ObjectId, ref: "Meal" }],
active: { type: Boolean, default: true },
date_ordered: { type: Date, default: () => Date.now() },
};
const Order = mongoose.model("Order", new mongoose.Schema(orderObject));
module.exports = { Order };
And this is my meal model:
const mongoose = require("mongoose");
const mealCategories = {
MAIN: "MAIN",
DESSERT: "DESSERT",
APPETIZER: "APPETIZER",
DRINK: "DRINK",
};
const mealTypes = {
REGULAR: "REGULAR",
SPECIAL: "SPECIAL",
};
const mealObject = {
name: { type: String },
ingredients: [
{ name: { type: String }, allergen: { type: Boolean, default: false } },
],
is_offered: { type: Boolean, default: false },
orders: [{ type: mongoose.Types.ObjectId, ref: "Order" }],
cook: { type: mongoose.Types.ObjectId, ref: "CookRoleUser" },
category: {
type: String,
enum: Object.values(mealCategories),
default: mealCategories.MAIN,
},
type: {
type: String,
enum: Object.values(mealTypes),
default: mealTypes.REGULAR,
},
date_created: { type: Date, default: () => Date.now() },
};
const mealSchema = new mongoose.Schema(mealObject);
const Meal = mongoose.model("Meal", mealSchema);
module.exports = { Meal, mealTypes };
I've tried to do this:
For addition:
const addMealsToOrder = async (req, res, next) => {
const order = await Order.findById(req.body.orderId);
let mealCount = 0;
await req.body.mealIds?.forEach(async (mealId, index) => {
const meal = await Meal.findById(mealId);
if (order.meals.includes(mealId)) {
return next(
new HttpError(
`Meal ${meal.name} already in order ${order.remark}.`,
400
)
);
}
if (order.cook.toString() != meal.cook.toString()) {
return next(
new HttpError(
`Meal ${meal.name} cook not same as order ${order.remark} cook.`
)
);
}
meal.orders.push(order.id);
order.meals.push(meal.id);
await meal.save();
mealCount = index;
});
if (mealCount == req.meals.length - 1) {
order.save();
res.json(order);
}
};
or without checking is are meals already in order and are cooks the same:
const addMealToOrderTest = async (req, res, next) => {
await req.body.mealIds.forEach(async (mealId) => {
const order = await Order.findById(req.body.orderId);
const meal = await Meal.findById(mealId);
order.meals.push(mealId);
meal.orders.push(order.id);
await order.save();
await meal.save();
});
res.json(2);
};
And for deletion:
const removeMealsFromOrder = async (req, res, next) => {
const { orderId, mealIds } = req.body;
const order = await Order.findById(orderId);
await req.body.mealIds.forEach(async (mealId, index) => {
const meal = Meal.findById(mealId);
meal.orders = meal.orders.filter((oId) => oId != order.id);
order.meals = order.meals.filter((mId) => mId != mealId);
await meal.save()
});
await order.save()
res.json(order);
};
I've tested adding and deleting in postman. Behaviour is very weird and random. Some times it does not add meals, or adds only few, etc. I test the result by fetching single order and/or single meal.
My question:
What is the best way to implement many-to-many update where you want to add for a single entity (order) multiple entities (meals)?
Furthermore I would also want to check, in addtion request, does the certain meal already exist in that order and are the order cook and certain meal cook not the same. If some of these condition is true, I want to inform the request issuer about it and stop all adding and in deletion request does the order contain certain meal and also inform the request issuer if that is true.
EDIT 04/08/22
Now i've tried to make my controllers as such
Addition
const addMealToOrderTest = async (req, res, next) => {
const { orderId, mealIds } = req.body;
await mealIds.forEach(async (mealId) => {
const order = await Order.findById(orderId);
const meal = await Meal.findById(mealId);
order.meals.push(mealId);
meal.orders.push(order.id);
await order.save();
await meal.save();
});
res.json(2);
};
Removal
const removeMealsFromOrder = async (req, res, next) => {
const { orderId, mealIds } = req.body;
await mealIds.forEach(async (mealId) => {
const order = await Order.findById(orderId);
const meal = await Meal.findById(mealId);
order.meals.pull(mealId);
meal.orders.pull(orderId);
await order.save();
await meal.save();
});
res.json(2);
};
Behaviour is very random. Sometimes it works all fine but sometimes I get:
VersionError if order.save() is in mealIds.forEach function or
ParrallelSaveError if the fetching and saving of the order are outside mealIds.forEach function.
EDIT no:2 04/08/22
Addition
const addMealToOrderTest = async (req, res, next) => {
const { orderId, mealIds } = req.body;
const order = await Order.findById(orderId);
let addedMealCount = 0;
mealIds.forEach((mealId) => {
if (!order.meals.includes(mealId)) {
order.meals.push(mealId);
addedMealCount += 1;
}
});
if (addedMealCount != mealIds.length) {
return res.json({ error: "Meal already in that order" });
}
await mealIds.forEach(async (mealId) => {
const meal = await Meal.findById(mealId);
if (!meal.orders.includes(mealId)) {
meal.orders.push(order.id);
await meal.save();
}
});
await order.save();
res.json(order);
};
Removal
const removeMealsFromOrder = async (req, res, next) => {
const { orderId, mealIds } = req.body;
const order = await Order.findById(orderId);
mealIds.forEach((mealId) => {
order.meals.pull(mealId);
});
await mealIds.forEach(async (mealId) => {
const meal = await Meal.findById(mealId);
meal.orders.pull(orderId);
await meal.save();
});
await order.save();
res.json(order);
};
Addition and removal both seem to work now. But I am wondering how could I check is meal cook and order cook the same when I add meals to order.
Problem with this is that I would have to fetch each meal, and then check are cooks the same as in the order.
When I do so, usually I get Cannot set headers or such. I've tried to initialize counter before forEach loop in which I fetch an meal for mealId and increment it only if cooks are the same. And if they are not, counter is not incremented and response is sent. I sent an ok response with order only if the counter is the same as mealIds length. But, counter does not seem to change if I fetch meal by id.

How to return the result of a mongodb pipeline by a get methodo?

I have a pipeline and its result I want to return it by an express method that is get or not i know if it is more advisable to send it by a socket
this is my file pipeline.js:
function getModel(model) {
model.aggregate([{
$group: {
_id: null,
"price": {
$sum: "$price",
}
}
}]).exec((e, d) => {
return JSON.stringify(d)
})
}
module.exports = getModel;
in the model.js file I'm going to call my pipeline.js file and therefore the function
model.js:
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const getModel = require('./pipeline');
const mySchema = new Schema({
user: {
type: Schema.ObjectId,
ref: 'User'
},
namepet: String,
type_of_service: String,
characteristic_of_pet: String,
price: Number
});
const model = mongoose.model('Cites', mySchema);
here is the function-> getModel(model);
module.exports = model;
and it works for me as I want the problem is that the result I have to send it by a method get and I have no idea how to do it
How can I send the result indicating the red arrow of the image by a get method?
var express = require('express');
var app = express();
function getModel(model) {
model.aggregate([{
$group: {
_id: null,
"price": {
$sum: "$price",
}
}
}]).exec((e, d) => {
return JSON.stringify(d)
})
}
app.get('/', function(req, res) {
console.log('marhaba');
res.send(getModel( ** Model ** ))) //== > here call the getModel function
});
app.listen(3000, function() {
console.log("Working on port 3000");
});

Mongoose statics is not working with async and await

I have one mongoose model where I declared 1 statics which should return me all features.
const Feature = require('./featureModel')
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const schemaOptions = {
timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' },
};
const ModemSchema = new Schema({
proxy_type: {
type: String,
default: 'None'
},
data_prioritization: {
type: String,
default: 'None'
}
}, schemaOptions);
ModemSchema.statics.possibleProxyTypes = async function () {
features = await Feature.find({})
return features
}
module.exports = mongoose.model('Modem', ModemSchema);
Modem.possibleProxyTypes I am calling it like this (class method)
But await is not waiting and I am getting output [AsyncFunction] . Not sure what is wrong here.
I made it worked like this. ( If you had added all the related codes to the question, I would say where was the problem.)
Modem Schema: ( almost no change, I only added let features, and console.log)
ModemSchema.statics.possibleProxyTypes = async function() {
letfeatures = await Feature.find({});
console.log(features);
return features;
};
And I tried this in a sample get route like this:
const Feature = require("../models/featureModel");
const Modem = require("../models/modemModel");
router.get("/modem", async (req, res) => {
const features = await Modem.possibleProxyTypes();
res.send(features);
});
Maybe the problem was that you didn't use await here in this line:
await Modem.possibleProxyTypes()
This returned me features like this:
[
{
"_id": "5e0207ff4323c7545026b82a",
"name": "Feature 1",
"__v": 0
},
{
"_id": "5e0208054323c7545026b82b",
"name": "Feature 2",
"__v": 0
}
]

How to use Mongoose with GraphQL and DataLoader?

I am using MongoDB as my database and GraphQL. I am using Mongoose for my model. I realised my GraphQL queries are slow because the same documents are being loaded over and over again. I would like to use DataLoader to solve my problem, but I don't know how.
Example
Let's say I have the following schema, describing users with friends :
// mongoose schema
const userSchema = new Schema({
name: String,
friendIds: [String],
})
userSchema.methods.friends = function() {
return User.where("_id").in(this.friendIds)
}
const User = mongoose.model("User", userSchema)
// GraphQL schema
const graphqlSchema = `
type User {
id: ID!
name: String
friends: [User]
}
type Query {
users: [User]
}
`
// GraphQL resolver
const resolver = {
Query: {
users: () => User.find()
}
}
Here is some example data in my database :
[
{ id: 1, name: "Alice", friendIds: [2, 3] },
{ id: 2, name: "Bob", friendIds: [1, 3] },
{ id: 3, name: "Charlie", friendIds: [2, 4, 5] },
{ id: 4, name: "David", friendIds: [1, 5] },
{ id: 5, name: "Ethan", friendIds: [1, 4, 2] },
]
When I do the following GraphQL query :
{
users {
name
friends {
name
}
}
}
each user is loaded many times. I would like each user Mongoose document to be loaded only once.
What doesn't work
Defining a "global" dataloader for fetching friends
If I change the friends method to :
// mongoose schema
const userSchema = new Schema({
name: String,
friendIds: [String]
})
userSchema.methods.friends = function() {
return userLoader.load(this.friendIds)
}
const User = mongoose.model("User", userSchema)
const userLoader = new Dataloader(userIds => {
const users = await User.where("_id").in(userIds)
const usersMap = new Map(users.map(user => [user.id, user]))
return userIds.map(userId => usersMap.get(userId))
})
then my users are cached forever rather than on a per request basis.
Defining the dataloader in the resolver
This seems more reasonable : one caching mechanism per request.
// GraphQL resolver
const resolver = {
Query: {
users: async () => {
const userLoader = new Dataloader(userIds => {
const users = await User.where("_id").in(userIds)
const usersMap = new Map(users.map(user => [user.id, user]))
return userIds.map(userId => usersMap.get(userId))
})
const userIds = await User.find().distinct("_id")
return userLoader.load(userIds)
}
}
}
However, userLoader is now undefined in the friends method in Mongoose schema. Let's move the schema in the resolver then!
// GraphQL resolver
const resolver = {
Query: {
users: async () => {
const userLoader = new Dataloader(userIds => {
const users = await User.where("_id").in(userIds)
const usersMap = new Map(users.map(user => [user.id, user]))
return userIds.map(userId => usersMap.get(userId))
})
const userSchema = new Schema({
name: String,
friendIds: [String]
})
userSchema.methods.friends = function() {
return userLoader.load(this.friendIds)
}
const User = mongoose.model("User", userSchema)
const userIds = await User.find().distinct("_id")
return userLoader.load(userIds)
}
}
}
Mh ... Now Mongoose is complaining on the second request : resolver gets called again, and Mongoose doesn't like 2 models being defined with the same model name.
"Virtual populate" feature are of no use, because I can't even tell Mongoose to fetch models through the dataloader rather than through the database directly.
Question
Has anyone had the same problem? Does anyone have a suggestion on how to use Mongoose and Dataloader in combination? Thanks.
Note: I know since my schema is "relational", I should be using a relational database rather than MongoDB. I was not the one to make that choice. I have to live with it until we can migrate.
Keep your mongoose schema in a separate module. You don't want to create your schema each request -- just the first time the module is imported.
const userSchema = new Schema({
name: String,
friendIds: [String]
})
const User = mongoose.model("User", userSchema)
module.exports = { User }
If you want, you can also export a function that creates your loader in the same module. Note, however, that we do not want to export an instance of a loader, just a function that will return one.
// ...
const getUserLoader = () => new DataLoader((userIds) => {
return User.find({ _id: { $in: userIds } }).execute()
})
module.exports = { User, getUserLoader }
Next, we want to include our loader in the context. How exactly this is done will depend on what library you're using to actually expose your graphql endpoint. In apollo-server, for example, context is passed in as part of your configuration.
new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => ({
userLoader: getUserLoader()
}),
})
This will ensure that we have a fresh instance of the loader created for each request. Now, your resolvers can just call the loader like this:
const resolvers = {
Query: {
users: async (root, args, { userLoader }) => {
// Our loader can't get all users, so let's use the model directly here
const allUsers = await User.find({})
// then tell the loader about the users we found
for (const user of allUsers) {
userLoader.prime(user.id, user);
}
// and finally return the result
return allUsers
}
},
User: {
friends: async (user, args, { userLoader }) => {
return userLoader.loadMany(user.friendIds)
},
},
}

Collection not saved mongodb nodejs

I have an issue on my application (mongodb/nodejs), my aim is to save a collection :
const Job = require("../models/Job");
exports.saveJob = (req, res, next) => {
const newJob = new Job(req.body);
newJob.gender = 'Male';
newJob.save((err, myjob) => {
myjob.code = '1234';
myjob.save((err, mysavedjob) => {
console.log(mysavedjob);
/** OUTPUT **
* { __v: 0,
updatedAt: 2018-07-31T08:31:47.664Z,
createdAt: 2018-07-31T08:31:47.664Z,
gender:'Male',
code:'1234',
...
}
*/
})
})
}
As you can see, the code output the saved document, but when I check the database , I don't have the code:'1234' I have just
{ __v: 0,
updatedAt: 2018-07-31T08:31:47.664Z,
createdAt: 2018-07-31T08:31:47.664Z,
gender:'Male',
...
}
==== UPDATE (adding the JobModel) ====
const mongoose = require("mongoose");
const JobSchema = new mongoose.Schema(
{
userId: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
code: String,
gender: String,
},
{ timestamps: true }
);
module.exports = mongoose.model("Job", JobSchema, "jobs");
Someone could tell me why please ?
thank you.
I am not sure this is the correct way to alter a job after saving it but try this
const Job = require("../models/Job");
exports.saveJob = (req, res, next) => {
const newJob = new Job(req.body);
newJob.gender = 'Male';
newJob.save((err, myjob) => {
myjob.code = '1234';
Job.findOneAndUpdate({_id: myjob._id}, myjob, (err, mysavedjob) => {
console.log(mysavedjob);
});
})
}

Resources