Storing notes with the LevelDB datastore

To get started with actual databases, let’s look at an extremely lightweight, small- footprint database engine: level. This is a Node.js-friendly wrapper that wraps around the LevelDB engine and was developed by Google. It is normally used in web browsers for local data persistence and is a non-indexed, NoSQL datastore originally designed for use in browsers. The Level Node.js module uses the LevelDB API and supports multiple backends, including leveldown, which integrates the C++ LevelDB database into Node.js.

To install the database engine, run the following command:

$ npm install level@6.x –save

This installs the version of level that the following code was written against. Then, create the models/notes-level.mjs module, which will contain the

AbstractNotesStore implementation:

import util from ‘util’;

import { Note, AbstractNotesStore } from ‘./Notes.mjs’;

import level from ‘level’;

import { default as DBG } from ‘debug’;

const debug = DBG(‘notes:notes-level’);

const error = DBG(‘notes:error-level’);

let db;

async function connectDB() {

if (typeof db !== ‘undefined’ || db) return db;

db = await level(

process.env.LEVELDB_LOCATION || ‘notes.level’, {

createIfMissing: true,

valueEncoding: “json”

});

return db;

}

We start the module with the import statements and a couple of declarations. The connectDB function is used for what the name suggests—to connect with a database. The createIfMissing option also does what it suggests, which is creating a database if there isn’t one already one with the name that is used. The import from the module, level, is a constructor function that creates a level instance connected to the database specified by the first argument. This first argument is a location in the filesystem—a directory, in other words—where the database will be stored.

The level constructor returns a db object through which to interact with the database. We’re storing db as a global variable in the module for ease of use. In connectDB, if the db object is set, we just return it immediately; otherwise, we open the database using the constructor, as just described.

The location of the database defaults to notes.level in the current directory. The LEVELDB_LOCATION environment variable can be set, as the name implies, to specify the database location.

Now, let’s add the rest of this module:

export default class LevelNotesStore extends AbstractNotesStore {

async close() {

const _db = db;

db = undefined;

return _db ? _db.close() : undefined;

}

async update(key, title, body) {

return crupdate(key, title, body);

}

async create(key, title, body) {

return crupdate(key, title, body);

}

async read(key) {

const db = await connectDB();

const note = Note.fromJSON(await db.get(key));

return note;

}

async destroy(key) {

const db = await connectDB();

await db.del(key);

}

async keylist() {

const db = await connectDB(); const keyz = [];

await new Promise((resolve, reject) => {

db.createKeyStream()

.on(‘data’, data => keyz.push(data))

.on(‘error’, err => reject(err))

.on(‘end’, () => resolve(keyz));

});

return keyz;

}

async count() {

const db = await connectDB(); var total = 0;

await new Promise((resolve, reject) => {

db.createKeyStream()

.on(‘data’, data => total++)

.on(‘error’, err => reject(err))

.on(‘end’, () => resolve(total));

});

return total;

}

}

async function crupdate(key, title, body) { const db = await connectDB();

const note = new Note(key, title, body); await db.put(key, note.JSON);

return note;

}

As expected, we’re creating a LevelNotesStore class to hold the functions.

In this case, we have code in the close function that calls db.close to close down the connection. The level documentation suggests that it is important to close the connection, so we’ll have to add something to app.mjs to ensure that the database closes when the server shuts down. The documentation also says that level does not support concurrent connections to the same database from multiple clients, meaning if we want multiple Notes instances to use the database, we should only have the connection open when necessary.

Once again, there is no difference between the create and update operations, and so we use a crupdate function again. Notice that the pattern in all the functions is to first call connectDB to get db, and then to call a function on the db object. In this case, we use db.put to store the Note object in the database.

In the read function, db.get is used to read the note. Since the Note data was stored as JSON, we use Note.fromJSON to decode and instantiate the Note instance.

The destroy function deletes a record from the database using the db.del function.

Both keylist and count use the createKeyStream function. This function uses an event-oriented interface to stream through every database entry, emitting events as it goes. A data event is emitted for each key in the database, while the end event is emitted at the end of the database, and the error event is emitted on errors. Since there is no simple way to present this as a simple async function, we have wrapped it with a Promise so that we can use await. We then invoke createKeyStream, letting it run its course and collect data as it goes. For keylist, in the data events, we add the data (in this case, the key to a database entry) to an array.

For count, we use a similar process, and in this case, we simply increment a counter. Since we have this wrapped in a Promise, in an error event, we call reject, and in an end event, we call resolve.

Then, we add the following to package.json in the scripts section:

“level-start”: “cross-env DEBUG=notes:* PORT=3000 NOTES_MODEL=level node ./app.mjs”,

Finally, you can run the Notes application:

$ npm run level-start

> notes@0.0.0 start /Users/david/chap07/notes

> cross-env DEBUG=notes:* PORT=3000 NOTES_MODEL=level node ./app.mjs

notes:server Listening on port 3000 +0ms

The printout in the console will be the same, and the application will also look the same. You can put it through its paces to check whether everything works correctly.

Since level does not support simultaneous access to a database from multiple instances, you won’t be able to use the multiple Notes application scenario. You will, however, be able to stop and restart the application whenever you want to without losing any notes.

Before we move on to looking at the next database, let’s deal with a issue mentioned earlier—closing the database connection when the process exits.

1. Closing database connections when closing the process

The level documentation says that we should close the database connection with db.close. Other database servers may well have the same requirement. Therefore, we should make sure we close the database connection before the process exits, and perhaps also on other conditions.

Node.js provides a mechanism to catch signals sent by the operating system. What we’ll do is configure listeners for these events, then close NotesStore in response.

Add the following code to appsupport.mjs:

import { NotesStore } from ‘./models/notes-store.mjs’;

async function catchProcessDeath() {

debug(‘urk…’);

await NotesStore.close();

await server.close();

process.exit(0);

}

process.on(‘SIGTERM’, catchProcessDeath);

rocess.on(‘SIGINT’, catchProcessDeath);

process.on(‘SIGHUP’, catchProcessDeath);

process.on(‘exit’, () => { debug(‘exiting…’); });

We import NotesStore so that we can call its methods, and server was already imported elsewhere.

The first three process.on calls listen to operating system signals. If you’re familiar with Unix process signals, these terms will be familiar. In each case, the event calls the catchProcessDeath function, which then calls the close function on NotesStore and, for good measure, on server.

Then, to have a measure of confirmation, we attached an exit listener so that we can print a message when the process is exiting. The Node.js documentation says that the exit listeners are prohibited from doing anything that requires further event processing, so we cannot close database connections in this handler.

Let’s try it out by running the Notes application and then immediately pressing Ctrl + C:

$ npm run level-start

> notes@0.0.0 level-start /home/david/Chapter07/notes

> cross-env DEBUG=notes:* PORT=3000 NOTES_MODEL=level node ./app.mjs

notes:debug Listening on port 3000 +0ms

^C notes:debug urk… +1s

notes:debug exiting… +3s 

Sure enough, upon pressing Ctrl + C, the exit and catchProcessDeath listeners are called.

That covers the level database, and we also have the beginning of a handler to gracefully shut down the application. The next database to cover is an embedded SQL database that requires no server processes.

Source: Herron David (2020), Node.js Web Development: Server-side web development made easy with Node 14 using practical examples, Packt Publishing.

Leave a Reply

Your email address will not be published. Required fields are marked *