Creating a Node module with TypeScript

TypeScript has been on my TODO of things to learn for absolutely ages, I’ve seen a lot of it when playing around with JavaScript libraries, looking at the source code of certain modules pretending to myself that I understand what I’m looking at.

I’ve finally given in, finally I’m coding some TypeScript. And I thought what better way to start than to add a brand new Node module, as of course, there are only more than
963,627 of the buggers. In the time it’s taken me to write this blog, 100 more modules have been added.

Inspiration

Discord were under fire recently for the way they cache images, unencrypted disk storage which wasn’t cleared when the images were removed from the server, people weren’t happy. So using this as a bit of inspiration I’m going to make a Node module which persists encrypted data to local disk storage. I’m still quite amazed that the module doesn’t already exist.

This image has an empty alt attribute; its file name is image.png

You can find GitHub repo for what I create in this blog here.

TypeScript setup

The first thing we need to do is setup the TypeScript config. So I added a file tsconfig.json. The two important things here are the declaration and outDir. “declaration”: true is for our Declaration files. And “outDir” : “./dist” is where these declaration files go along with our transpiled JavaScript code. This dist folder is what npm will eventually use to build our package!

{
"compilerOptions": {
"removeComments": true,
"preserveConstEnums": true,
"sourceMap": true,
"declaration": true,
"outDir": "./dist"
}
}

Note: It’s also just kind of nice to have the transpiled code out of the way, I know intellij by default sets the .ts files as folders with files containing the .map and .js files, I don’t love this, it hurts my OCD 😀

The interface

The first thing I did was initialise my node project, the same way as you usually would with the classic npm init. I know that my module is going to do 4 things, pretty much the same as a normal persistence library would. I’m going to set, get, remove and clear. So I will create a TypeScript interface with each of these functions.

export interface EncryptedCache {

    get(id : string): Promise<string>;

    set(id: string, value: string): Promise<boolean>;

    remove(id: string) : Promise<boolean>;

    clear() : Promise<boolean>;
}
  • get will take a string which is the id of a cached item it will return a Promise of type string, it’s 2019 now, people love their promises.
  • set will take the string id of the cache and the string value, obviously if you’re gonna plonk in an object here you’d do something like JSON.stringify(myData) beforehand
  • remove takes in that id again and removes it from the cache
  • clear emptys the cache altogether

The implementation

We need to create an index.ts. This will contain the concrete implementations of the interface functions specified above. This class has a constructor which takes a string for the folder name of the cache and a string for the secret key which is used for encrypting. I have heavily commented each api to explain it a little further.

export class FileSystemCache implements EncryptedCache {



    readonly cryptr: Cryptr;
    readonly folderName: string;
    readonly directoryPath: string;

    constructor(folderName: string, secretKey: string) {
        this.cryptr = new Cryptr(secretKey);
        this.folderName = folderName;
        this.directoryPath = path.normalize(os.tmpdir() + "/" + this.folderName);

        if (!fs.existsSync(this.directoryPath)) {
            fs.mkdirSync(this.directoryPath);
        }
    }

    /**
     * Encrypt an item and put it in the cache
     * @param {string} id
     * @param {string} value
     * @returns {Promise<boolean>}
     */
    set(id: string, value: string): Promise<boolean> {
        const encryptedValue = this.encrypt(value);
        return new Promise<boolean>((resolve, reject) => {
            return fs.writeFile(path.normalize(this.directoryPath + "/") + id, encryptedValue, (err) => {
                err ? reject(false) : resolve(true);
            });

        });
    }

    /**
     * Retrieve an item from the cache and decrypt it
     * @param {string} id
     * @returns {Promise<string>}
     */
    get(id: string): Promise<string> {
        return new Promise<string>((resolve, reject) => {
            return fs.readFile(path.normalize(this.directoryPath + "/") + id, (err, data) => {
                err ? reject(false) : resolve(this.decrypt(data.toString()));
            });
        });
    }

    /**
     * Delete an item in the cache using the specified id
     * @param {string} id
     * @returns {Promise<boolean>}
     */
    remove(id: string): Promise<boolean> {
        return new Promise<boolean>((resolve, reject) => {
            let fileToRemove = `${this.directoryPath}/${id}`;
            fileToRemove = path.normalize(fileToRemove);
            fs.unlink(fileToRemove, err => {
                err ? reject(false) : resolve(true);
            });
        });
    }

    /**
     * Clears all the files within the cache directory
     * @returns {Promise<boolean>}
     */
    clear(): Promise<boolean> {
        return new Promise<boolean>((resolve, reject) => {

            fs.readdir(this.directoryPath, (err, files) => {
                if (err) {
                    reject(false);
                }
                const removeFilePromises = this.getRemoveFilePromises(files);
                Promise.all(removeFilePromises).then(() => {
                    resolve(true);
                }).catch(() => {
                    reject(false);
                });
            });

        });
    }


    private getRemoveFilePromises(files) {
        const removeFilePromises: Array<Promise<boolean>> = [];

        for (const file of files) {
            removeFilePromises.push(this.remove(file));
        }
        return removeFilePromises;
    }

    private encrypt(value): string {
        return this.cryptr.encrypt(JSON.stringify(value));
    }

    private decrypt(value): string {
        return JSON.parse(this.cryptr.decrypt(value));
    }
}

Next I added a tonne of tests using Jest. You can have a look at these on the GitHub repo if they interest you, I suspect the vast majority of you couldn’t give a shite, so I won’t add them here.

Adding Travis

Every node module needs to have the sexy little badge in the README.md to let everyone know that the build is passing.

To do that, it’s bloody simple. All you have to do is add a .travis.yml to your project and when you sign up to https://travis-ci.com/ using your GitHub auth it’s as simple as copying the badge’s html. This is an example of what my .yml file looks like, I probably don’t need to use node 10, but I’ve kept this as simple as possible.

language: node_js
node_js:
- 10
install:
- npm install
script:
- npm test
- npm run coveralls

Publishing the node module

Now that we have our complete Node module it’s time to publish! Because npm only really deals with my .js and .d.ts files you can add a .npmignore file. Mine looks like this. src/ is where my .ts files are and __tests__/ are where my tests.ts files are.

src/
__tests__/
  1. Use npm login or npm adduser if you don’t already have an account
  2. write npm publish to get her published!

Then you can simply npm install my-module-name and you’re good to go. You can find my npm package here.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s