The filesystem is an often overlooked database engine. While filesystems don't have the sort of query features supported by database engines, they are a reliable place to store files. The notes schema is simple enough that the filesystem can easily serve as its data storage layer.
Let's start by adding a function to Note.mjs:
export default class Note {
...
get JSON() {
return JSON.stringify({ key: this.key, title: this.title, body: this.body }); }
static fromJSON(json) { var data = JSON.parse(json); var note = new Note(data.key, data.title, data.body); return note; }
}
JSON is a getter, which means it gets the value of the object. In this case, the note.JSON attribute/getter, no parentheses, will simply give us the JSON representation of the Note. We'll use this later for writing to JSON files.
fromJSON is a static function, or factory method, to aid in constructing Note objects if we have a JSON string. The difference is that JSON is associated with an instance of the Note class, while fromJSON is associated with the class itself. The two can be used as follows:
const note = new Note("key", "title", "body");
const json = note.JSON; // produces JSON text
const newnote = Note.fromJSON(json); // produces new Note instance
Now, let's create a new module, models/notes-fs.mjs, to hold the filesystem model:
import fs from 'fs-extra';
import path from 'path';
import util from 'util';
import Note from './Note';
import DBG from 'debug';
const debug = DBG('notes:notes-fs');
const error = DBG('notes:error-fs');
async function notesDir() {
const dir = process.env.NOTES_FS_DIR || "notes-fs-data";
await fs.ensureDir(dir);
return dir;
}
function filePath(notesdir, key) { return path.join(notesdir, `${key}.json`); }
async function readJSON(notesdir, key) {
const readFrom = filePath(notesdir, key);
var data = await fs.readFile(readFrom, 'utf8');
return Note.fromJSON(data);
}
The notesDir function will be used throughout notes-fs to ensure that the directory exists. To make this simple, we're using the fs-extra module because it adds Promise-based functions to the fs module (https://www.npmjs.com/package/fs-extra). In this case, fs.ensureDir verifies whether the named directory structure exists, and, if not, the directory path is created.
The environment variable, NOTES_FS_DIR, configures a directory within which to store notes. We'll have one file per note and store the note as JSON. If no environment variable is specified, we'll fall back on using notes-fs-data as the directory name.
Because we're adding another dependency:
$ npm install fs-extra --save
The filename for each data file is the key with .json appended. That gives one limitation that filenames cannot contain the / character, so we test for that using the following code:
async function crupdate(key, title, body) {
var notesdir = await notesDir();
if (key.indexOf('/') >= 0)
throw new Error(`key ${key} cannot contain '/'`);
var note = new Note(key, title, body);
const writeTo = filePath(notesdir, key);
const writeJSON = note.JSON;
await fs.writeFile(writeTo, writeJSON, 'utf8');
return note;
}
export function create(key, title, body) { return crupdate(key, title, body); }
export function update(key, title, body) { return crupdate(key, title, body); }
As is the case with the notes-memory module, the create and update functions use the exact same code. The notesDir function is used to ensure that the directory exists, then we create a Note object, and then write the data to a file.
Notice how the code is very straightforward because of the async function. We aren't checking for errors because they'll be automatically caught by the async function and bubble out to our caller:
export async function read(key) {
var notesdir = await notesDir();
var thenote = await readJSON(notesdir, key);
return thenote;
}
Using readJSON, read the file from the disk. It already generates the Note object, so all we have to do is return that object:
export async function destroy(key) {
var notesdir = await notesDir();
await fs.unlink(filePath(notesdir, key));
}
The fs.unlink function deletes our file. Because this module uses the filesystem, deleting the file is all that's necessary to delete the note object:
export async function keylist() {
var notesdir = await notesDir();
var filez = await fs.readdir(notesdir);
if (!filez || typeof filez === 'undefined') filez = [];
var thenotes = filez.map(async fname => {
var key = path.basename(fname, '.json');
var thenote = await readJSON(notesdir, key);
return thenote.key;
});
return Promise.all(thenotes);
}
The contract for keylist is to return a Promise that will resolve to an array of keys for existing note objects. Since they're stored as individual files in the notesdir, we have to read every file in that directory to retrieve its key.
Array.map constructs a new array from an existing array, namely the array of filenames returned by fs.readdir. Each entry in the constructed array is the async function, which reads the Note, returning the key:
export async function count() {
var notesdir = await notesDir();
var filez = await fs.readdir(notesdir);
return filez.length;
}
export async function close() { }
Counting the number of notes is simply a matter of counting the number of files in notesdir.