I am building tests for my node/express controller methods and using #shelf/jest-mongodb. I am creating a document first, and then when I try to find that I have to run find twice from model in order to get the results. It should get the results in the first find instead.
test.js
const { Subscription } = require('../src/models/subscription.schemaModel'); // model
const {
createSubscription,
} = require('../src/controllers/subscription.controller');
const subData = {...};
beforeAll(async () => {
await mongoose.connect(
process.env.MONGO_URL,
{ useNewUrlParser: true, useUnifiedTopology: true },
(err) => {
if (err) {
console.error(err);
process.exit(1);
}
}
);
});
afterAll(async () => {
await mongoose.connection.close();
});
describe('creates a subscription ', () => {
it('can be created correctly', async () => {
const sub = await createSubscription(subData);
await Subscription.find(); // if I comment out this line, I would get 0 results.
const subs = await Subscription.find();
expect(subs[0].items[0].sku).toBe(233234);
});
});
subscription.controller.js
const Mongoose = require('mongoose');
const { Subscription } = require('../models/subscription.schemaModel');
const isTestEnv = process.env.NODE_ENV === 'test';
module.exports.createSubscription = async (data) => {
try {
let error = null;
const doc = new Subscription(data);
doc.accountId = Mongoose.Types.ObjectId(doc.accountId);
await doc.save(function (err) {
if (err) {
logger.error(`createSubscription saving ${err}`);
error = err;
}
});
if (!error) {
logger.info(
`Subscription created => id: ${doc._id} store: ${doc.store}`
);
return doc;
} else {
return error;
}
} catch (err) {
logger.error(`createSubscription ${err}`);
}
};
The schemaModel file essentially contains the schema and exports model. Everything seems to work fine if I would do all the operations in the test file (schema+model+controller module)which defeats the purpose of testing my modules but not if I am importing. In this case I would have to run find() twice to get the results.
I have been trying multiple things from what I could find from googling, but no luck! Any help or lead would be appreciated. Also let me know if you need any other details.
Thank you!!
The only problem that posted code contains is that Mongoose promise API is mixed with legacy callback API. It appears that save results in race condition that is has been circumvented by random delay that extra find provides.
Although Mongoose documentation mentions that methods unconditionally return promises, a common pattern for JavaScript APIs that support both promises and callbacks is to enable promise control flow by omitting callback argument, and vice versa. This is most likely what happens here.
A way to avoid race conditions in such cases is to stick to promise control flow, e.g.:
beforeAll(async () => {
try {
await mongoose.connect(
process.env.MONGO_URL,
{ useNewUrlParser: true, useUnifiedTopology: true },
)
} catch (err) {
console.error(err);
process.exit(1);
}
});
Related
I am trying to share the Mongo connection with other modules in my Node.js project. I keep getting either undefined or is not a function when attempting to use the exported client. I also had a question around detecting if the connection is in fact open before performing operations on the database.
It seems like using the app.locals would be the proper way to share the connection but I could not get that working either. Below is what I have at the moment. I've tried this many ways. Most of what I can find online seems to export the Mongo Node driver's method, not the connection itself. The idea is to connect once and never disconnect until the app shuts down.
const client = new MongoClient(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
async function connect () {
app.locals.dbConnected = false;
try {
await client.connect();
app.locals.dbConnected = true;
module.exports = client;
} catch (e) {
console.error(e);
}
};
then in another module do something like:
await client.db('syslogs').collection('production').insertOne(doc);
Is it possible to share the connection?
Could do something like below:
const client = new MongoClient(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
let __inst = null;
export default new Promise((resolve, reject) => {
if (__inst !== null) resolve(__inst);
// the open event is the key here
// like this we can handle error, close etc thru events as well
client.open((err, mongoInst) => {
if (err) reject(err);
__inst = mongoInst;
resolve(__inst);
});
});
Then in other module you can use the export client like you want.
Thanks.
I just got it working using app.locals.
index.js
const { MongoClient } = require("mongodb");
const client = new MongoClient(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
(async () => {
app.locals.dbConnected = false;
try {
await client.connect();
console.log("Connected to DB");
app.locals.client = client;
app.listen(PORT, HOST, () => {
console.log(`Running on http://${HOST}:${PORT}`);
});
} catch (e) {
console.error(e);
}
})();
Then in my module:
async function index (req, res) {
try {
let db = req.app.locals.client.db("admin");
await db.command({ ping: 1 });
console.log("pinged admin database");
}catch(err) {
console.log(err);
}
}
const addUSDCurrencyToCreatorPage = async () => {
try {
const page = await Pages.find({ username });
console.log("page --", page);
page.currency = "USD";
await page.save();
} catch (error) {
console.log(error);
}
};
I have this script in my Nodejs application where the try to perform the above function i.e. to add currency to the document.
When I run this script I get this error.
page.save() is not a function
How do I resolve this?
The Page returned from the find() callback will be an array of mongoose documents, hence the error page.save() is not a function.
Since page is an array you can use map() function like this:
page.map(async (p) => {
p.currency = "USD";
await p.save();
});
OR
Either use the findOne() method which returns a single Mongoose document that has the save method or use findOneAndUpdate() for an atomic update. Read more here
Example with findOneAndUpdate():
const addUSDCurrencyToCreatorPage = async () => {
try {
const page = await Pages.findOneAndUpdate({username},{ 'currency', 'USD' } }, { new: true }).exec();
// If `new` isn't true, `findOneAndUpdate()` will return the document as it was before it was updated.
console.log("page --", page);
} catch (error) {
console.log(error);
}
};
I am following a tutorial to make a blog, and for the MongoDB connection in the server.js file, the instructor made a boiler connection function withDB. Operations and res are props of withDB function. In line 6, is operations a function passed a prop of the withDB functions?
Below is the withDB function.
const withDB = async (operations, res) => {
try {
const client = await MongoClient.connect('mongodb://localhost:27017', { useNewUrlParser: true });
const db = client.db('my-blog');
await operations(db); // is operations a function that takes db as its props?
client.close();
} catch (error) {
res.status(500).json({ message: 'Error connecting to db', error });
}
}
Using withDB in a function
app.get('/api/articles/:name', async (req, res) => {
withDB(async (db) => {
const articleName = req.params.name;
const articleInfo = await db.collection('articles').findOne({ name: articleName })
res.status(200).json(articleInfo);
}, res);
})
yes actually operations is your callback function, you call it with db as param once you initialize your database connection.
Maybe you're not comfortable with ES6 arrow function syntax. you can find in Mdn doc a simple example with old regular function, and in your case it could be :
function findArticlesByName(articleName) {
return function(db) {
return db.collection('articles').findOne({ name:
articleName });
}
}
async function withDB(callback) {
try {
const client = await MongoClient.connect('mongodb://localhost:27017', { useNewUrlParser: true });
const db = client.db('my-blog');
return callback(db);
} catch (error) {
throw new Error({ message: 'Error connecting to db', error });
} finally {
client?.close();
}
}
app.get('/api/articles/:name', async (req, res) => {
try {
const articleInfo = await withDB(findArticlesByName(req.params.name));
res.status(200).json(articleInfo);
} catch(error) {
res.status(500).json(error);
}
})
Conclusion, you could easily inline your callback function like in your example, but maybe it's more understandable that way.
Moreover, you should avoid to use a wrapper in order to create and close your db connection after each request, because it could occur some weird errors. Databases connections or any resource-intensive tasks should be shared as much as possible.
So a better solution is to create a specific class with the default implementation of your singleton, construct a single instance of your connection at the top of your app, pass the single instance into each module that needs it then close your connection just before exiting your app.
Hope it'll help.
I am playing around with promises, I have the following code to access my mongodb:
MongoClient.connect(url, { useUnifiedTopology: true })
.then(client => {
const db = client.db(dbName);
return db.collection('dogs');
})
.then(collection => collection.find().toArray())
.then(array => console.log(array))
// Client is not defined, how do I access it?
.finally(() => client.close())
.catch(error => {
console.log(error);
});
I can't access the client inside finally. Is there a good pattern to achieve this?
You can either use async/await:
const client = await MongoClient.connect(url, { useUnifiedTopology: true })
const db = client.db(dbName);
const dogs = db.collection('dogs');
...
When you have operations like find you can await them or using then as you did. If you are not very confident with promises, asyn/await might be clearer for you.
You can also await chained promises with then...
With async/await you can use try/catch/finally, that might be an easier solution for you.
You can wrap your code with an async function in order to use await, and create a variable before the try and catch to save the client, the code will be as follows:
const functionName = async () => {
let client;
try {
client = await MongoClient.connect(url, { useUnifiedTopology: true });
const db = client.db(dbName);
const collection = db.collection("dogs");
const array = collection.find().toArray();
console.log(array);
} catch (error) {
console.log(error);
} finally {
client.close();
}
};
I've been reading some CRUD / Mongoose guides, but haven't a good explainer for conditionally updating fields.
So for example, an action called updateItem is used in one place to update item.price but in another place it updates item.color. Does anyone know a good explanation or tutorial for Mongoose CRUD APIs that shows this?
I'm getting the blow code to work fine, but I have a feeling it could be cleaner :)
Thanks!!!
router.put('/tasks/:id', (req, res) => {
Task.findByIdAndUpdate(req.params.id,
req.body.owner ? { owner: req.body.owner } : { hours: req.body.hours }, { new: true })
.then(task => {
res.status(201).json(task)
})
.catch(err => {
console.log('Our error', err)
})
});
Another approach you could take is to first retrieve the object, and then only update the value if it is passed into the put request. An example of that could be something like this:
router.put('/tasks/:id', (req, res) => {
let price = req.body.price;
let color = req.body.color;
Task.findById(req.params.id, function (err, task) {
if (err) return handleError(err);
task.color = color || task.color;
task.price = price || task.price;
task.save(function(err, updatedTask) {
if err return handleError(err);
return res.send(updatedTask);
});
});
});
Here's another cleaner approach using async-await functions:
// Import promisify from utils
const promisify = require('utils').promisify;
// Wrap findByIdAndUpdate into a promise
const updateOwnerPromise = promisify(Task.findByIdAndUpdate);
// Write an async handler now
updateOwnerPromiseAsync = async (req, res) => {
const replacementObject = req.body.owner ? { owner: req.body.owner } : { hours: req.body.hours };
try {
await updateOwnerPromise(replacementObject, { new:true} );
return res.status(200).send({ message: 'Owner updated successfully!' });
} catch(err) {
// TODO: handle error here
console.log('Our error', err)
return res.status(500).send({ message: 'Failed to update owner, because of some issue at the server!' });
}
}
// Modify the express route with the handler
router.put('/tasks/:id', updateOwnerPromiseAsync);