Now that you've proved you have a working MongoDB server, let's get to work.
Installing the Node.js driver is as simple as running the following command:
$ npm install mongodb@3.x --save
Now create a new file, models/notes-mongodb.mjs:
import util from 'util';
import Note from './Note';
import mongodb from 'mongodb';
const MongoClient = mongodb.MongoClient;
import DBG from 'debug';
const debug = DBG('notes:notes-mongodb');
const error = DBG('notes:error-mongodb');
var client;
async function connectDB() {
if (!client) client = await MongoClient.connect(process.env.MONGO_URL);
return {
db: client.db(process.env.MONGO_DBNAME),
client: client
};
}
The MongoClient class is used to connect with a MongoDB instance. The required URL, which will be specified through an environment variable, uses a straightforward format: mongodb://localhost/. The database name is specified via another environment variable.
http://mongodb.github.io/node-mongodb-native/2.2/api/MongoClient.html
for MongoClient and http://mongodb.github.io/node-mongodb-native/2.2/api/Db.html for Db
This creates the database client, and then opens the database connection. Both objects are returned from connectDB in an anonymous object. The general pattern for MongoDB operations is as follows:
(async () => {
const client = await MongoClient.connect(process.env.MONGO_URL);
const db = client.db(process.env.MONGO_DBNAME);
// perform database operations using db object
client.close();
})();
Therefore, our model methods require both client and db objects, because they will use both. Let's see how that's done:
export async function create(key, title, body) {
const { db, client } = await connectDB();
const note = new Note(key, title, body);
const collection = db.collection('notes');
await collection.insertOne({ notekey: key, title, body });
return note;
}
export async function update(key, title, body) {
const { db, client } = await connectDB();
const note = new Note(key, title, body);
const collection = db.collection('notes');
await collection.updateOne({ notekey: key }, { $set: { title, body } });
return note;
}
We retrieve db and client into individual variables using a destructuring assignment.
MongoDB stores all documents in collections. A collection is a group of related documents, and a collection is analogous to a table in a relational database. This means creating a new document or updating an existing one starts by constructing it as a JavaScript object, and then asking MongoDB to save that object to the database. MongoDB automatically encodes the object into its internal representation.
The db.collection method gives us a Collection object with which we can manipulate the named collection. See its documentation at http://mongodb.github.io/node-mongodb-native/2.2/api/Collection.html.
As the method name implies, insertOne inserts one document into the collection. Likewise, the updateOne method first finds a document (in this case, by looking up the document with the matching notekey field), and then changes fields in the document as specified.
You'll see that these methods return a Promise. The mongodb driver supports both callbacks and Promises. Many methods will invoke the callback function if one is provided, otherwise it returns a Promise that will deliver the results or errors. And, of course, since we're using async functions, the await keyword makes this so clean.
Insert: https://docs.mongodb.org/getting-started/node/insert/.
Update: https://docs.mongodb.org/getting-started/node/update/.
Next, let's look at reading a note from MongoDB:
export async function read(key) {
const { db, client } = await connectDB();
const collection = db.collection('notes');
const doc = await collection.findOne({ notekey: key });
const note = new Note(doc.notekey, doc.title, doc.body);
return note;
}
The mongodb driver supports several variants of find operations. In this case, the Notes application ensures that there is exactly one document matching a given key. Therefore, we can use the findOne method. As the name implies, findOne will return the first matching document.
The argument to findOne is a query descriptor. This simple query looks for documents whose notekey field matches the requested key. An empty query will, of course, match all documents in the collection. You can match against other fields in a similar way, and the query descriptor can do much more. For documentation on queries, visit https://docs.mongodb.org/getting-started/node/query/.
The insertOne method we used earlier also took the same kind of query descriptor.
In order to satisfy the contract for this function, we create a Note object and then return it to the caller. Hence, we create a Note using the data retrieved from the database:
export async function destroy(key) {
const { db, client } = await connectDB();
const collection = db.collection('notes');
await collection.findOneAndDelete({ notekey: key });
}
One of the find variants is findOneAndDelete. As the name implies, it finds one document matching the query descriptor, and then deletes that document:
export async function keylist() {
const { db, client } = await connectDB();
const collection = db.collection('notes');
const keyz = await new Promise((resolve, reject) => {
var keyz = [];
collection.find({}).forEach(
note => { keyz.push(note.notekey); },
err => {
if (err) reject(err);
else resolve(keyz);
}
);
});
return keyz;
}
Here, we're using the base find operation and giving it an empty query so that it matches every document. What we're to return is an array containing the notekey for every document.
All of the find operations return a Cursor object. The documentation can be found at http://mongodb.github.io/node-mongodb-native/2.1/api/Cursor.html.
The Cursor object is, as the name implies, a pointer into a result set from a query. It has a number of useful functions related to operating on a result set. For example, you can skip the first few items in the results, or limit the size of the result set, or perform the filter and map operations.
The Cursor.forEach method takes two callback functions. The first is called on every element in the result set. In this case, we can use that to record just the notekey in an array. The second callback is called after all elements in the result set have been processed. We use this to indicate success or failure, and to return the keyz array.
Because forEach uses this pattern, it does not have an option for supplying a Promise, and we have to create the Promise ourselves, as shown here:
export async function count() {
const { db, client } = await connectDB();
const collection = db.collection('notes');
const count = await collection.count({});
return count;
}
export async function close() {
if (client) client.close();
client = undefined;
}
The count method takes a query descriptor and, as the name implies, counts the number of matching documents.