To get started with actual databases, let's look at an extremely lightweight, small-footprint database engine: LevelUP. This is a Node.js-friendly wrapper around the LevelDB engine developed by Google, which is normally used in web browsers for local data persistence. It is a non-indexed, NoSQL data store designed originally for use in browsers. The Node.js module, Level, uses the LevelDB API, and supports multiple backends, including LevelDOWN, which integrates the C++ LevelDB database into Node.js.
Visit https://www.npmjs.com/package/level for information on the module. The level package automatically sets up the levelup and leveldown packages.
To install the database engine, run this command:
$ npm install level@2.1.x --save
Then, start creating the models/notes-level.mjs module:
import fs from 'fs-extra';
import path from 'path';
import util from 'util';
import Note from './Note';
import level from 'level';
import DBG from 'debug';
const debug = DBG('notes:notes-level');
const error = DBG('notes:error-level');
var 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;
}
The level module gives us a db object through which to interact with the database. We're storing that object as a global within the module for ease of use. If the db object is set, we can just return it immediately. Otherwise, we open the database using createIfMissing to go ahead and create the database if needed.
The location of the database defaults to notes.level in the current directory. The environment variable LEVELDB_LOCATION can be set, as the name implies, to specify the database location:
async function crupdate(key, title, body) {
const db = await connectDB();
var note = new Note(key, title, body);
await db.put(key, note.JSON);
return note;
}
export function create(key, title, body) {
return crupdate(key, title, body);
}
export function update(key, title, body) {
return crupdate(key, title, body);
}
Calling db.put either creates a new database entry, or replaces an existing one. Therefore, both update and create are set to be the same function. We convert the Note to JSON so it can be easily stored in the database:
export async function read(key) {
const db = await connectDB();
var note = Note.fromJSON(await db.get(key));
return new Note(note.key, note.title, note.body);
}
Reading a Note is easy: just call db.get and it retrieves the data, which must be decoded from the JSON representation.
Notice that db.get and db.put did not take a callback function, and that we use await to get the results value. The functions exported by level can take a callback function, in which the callback will be invoked. Alternatively, if no callback function is provided, the level function will instead return a Promise for compatibility with async functions:
export async function destroy(key) {
const db = await connectDB();
await db.del(key);
}
The db.destroy function deletes a record from the database:
export async function keylist() {
const db = await connectDB();
var 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;
}
export async function 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;
}
export async function close() {
var _db = db;
db = undefined;
return _db ? _db.close() : undefined;
}
The createKeyStream function uses an event-oriented interface similar to the Streams API. It will stream through every database entry, emitting events as it goes. A data event is emitted for every key in the database, while the end event is emitted at the end of the database, and the error event is emitted on errors. The effect is that there's no simple way to present this as a simple Promise. Instead, we invoke createKeyStream, let it run its course, collecting data as it goes. We have to wrap it inside a Promise object, and call resolve on the end event.
Then we add this to package.json in the scripts section:
"start-level": "DEBUG=notes:* NOTES_MODEL=level node --experimental-modules ./bin/www.mjs",
Finally, you can run the Notes application:
$ DEBUG=notes:* npm run start-level > notes@0.0.0 start /Users/david/chap07/notes > node ./bin/www 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 and see that 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 at will without losing any notes.