Graduate Program KB

Matthew's Digging Into Node Notes


Table of Contents


Command Line Scripts

When creating command line scripts, we first set the top line in program, to allow it to utilise node (see below).

#! /usr/bin/env node
'use strict'

var args = require('minimist')(process.argv.slice(2), {
  boolean: ['help'],
  string: ['file'],
})

What this does, is take the first two arguments provided when the file is executed and place them into a variable. Additionally it sets some precedents for these arguments. The first one is a boolean (ie is it present or not, true if given, false if omitted). The second being a string of type file, so that the user knows what it should be.

Useful to Know:

  • Minimist: A library for processing arguments. It has no dependencies, but almost every other package depends on it. It returns an object with structure.
  • process.argv contains all arguments passed to a script
  • __dirname is a variable which contains the absolute path of the currently executing script
  • process.cwd() returns pwd

At the end of the exercise, our file looks like this:

bin/env node
"use strict";

var fs = require("fs");
var path = require("path");

var getStdin = require("get-stdin");

var args = require("minimist")(process.argv.slice(2), {
  boolean: ["help", "in"],
  string: ["file"],
});

var BASE_PATH = path.resolve(process.env.BASE_PATH || __dirname);

if (process.env.HELLO) {
  console.log(process.env.HELLO);
}

if (args.help) {
  printHelp();
} else if (args.in || args._.includes("-")) {
  getStdin().then(processFile).catch(error);
} else if (args.file) {
  fs.readFile(
    path.join(BASE_PATH, args.file),
    function onContents(err, contents) {
      if (err) {
        error(err.toString);
      } else {
        processFile(contents.toString());
      }
    }
  );
} else {
  error("Incorrect usage", true);
}

function processFile(contents) {
  contents = contents.toUpperCase();
  process.stdout.write(contents);
}
function error(msg, includeHelp = false) {
  console.error(msg);
  if (includeHelp) {
    console.log("");
    printHelp();
  }
}

function printHelp() {
  console.log("Hello! This prints when help is required");
}

Streams

Streams are unidirectional. They can be either readable or writable but not both. Duplex streams are those which are both readable and writable.

let path = require('path')
let fs = require('fs')

let readStream = fs.createReadStream(filePath)
let writeStream = fs.createWriteStream(filePath)

let outStream = readStream
outStream.pipe(process.stdout) // Writes input to stdout
  • Transform allows us to intercept the pipe and process items/chunks one at a time
    • In this example, chunk is a buffer from the stream
    • The callback function is used to notify when the transform is complete
    • We are converting chunks to string and then making them uppercase
let Transform = require('stream').Transform

let upperStream = new Transform({
  transform(chunk, encoding, callback) {
    this.push(chunk.toString().toUpperCase())
    callback()
  },
})

let outStream = outStream.pipe(upperStream)
outStream.pipe(process.stdout)
  • Zipping and Unzipping
    • We can create zipping and unzipping streams
    • We change the file type of the compressed file to .gz as this is the normal convention
let zlib = require('zlib')

let gzipStream = zlib.createGzip()
let outStream = outStream.pipe(gzipStream)
outputFile = `{outputFile}.gz`

let gunzipStream = zlib.createGunzip()
let outStream = outStream.pipe(gunzipStream)
  • To determine the end of a stream, use a promise
function streamComplete(stream) {
  return new Promise(function callback(response) {
    stream.on('end', response)
  })
}

outStream.pipe(process.stdout)
await streamComplete(outStream)

Database

For this demonstration, sqlite3 was used. This type can be maintained directly from the program itself.

let sqlite3 = require('sqlite3')
let path = require('path')

const DB_PATH = path.join(__dirname, 'my.db')
const DB_SQL_PATH = path.join(__dirname, 'mydb.sql')

var myDB = new sqlite3.Database(DB_PATH)
SQL3 = {
  run(...args) {
    return new Promise(function c(resolve, reject) {
      myDB.run(...args, function onResult(err) {
        if (err) reject(err)
        else resolve(this)
      })
    })
  },
  // Define a promise interface for each callback
  get: util.promisify(myDB.get.bind(myDB)),
  all: util.promisify(myDB.all.bind(myDB)),
  exec: util.promisify(myDB.exec.bind(myDB)),
}

We can then create some functions to directly interact with the database. An example below is getAllRecords, which as the name suggests, takes all the records in the database and returns them back to the main function which outputs them (provided records are found).

async function getAllRecords() {
  let result = await SQL3.all(
    `
			SELECT
				Other.data AS 'other',
				Something.data AS 'something'
			FROM 
				Something JOIN Other 
				ON (Something.otherID = Other.id)
			ORDER BY
				Other.id DESC, Something.data ASC 	
		`,
  )

  if (result && result.length > 0) {
    return result
  }
}

Webservers

We can setup simple webservers to serve static pages or content. An example of setting up a simple web server is depicted below:

const http = require('http')
// Create server
const httpserve = http.createServer(handleRequest)
const HTTP_PORT = 8039

main()

function main() {
  httpserv.listen(HTTP_PORT)
  console.log(`Listening on http://localhost:${HTTP_PORT}...`)
}

async function handleRequest(req, res) {
  if (req.url == '/hello') {
    // Write to header
    res.writeHead(200, { 'Content-Type': 'text/plain' })
    res.write('Hello World')
    res.end() // Close stream
  } else {
    res.writeHead(401)
    res.end()
  }
}

We can also configure a simple static alias server to allow access under the web root directory only. This file server uses regex to check incoming requests and serve different pages dependant on the outcome.

let path = require("path");
let http = require("http");
let staticAlias = require("node-static-alias");

const WEB_PATH = path.join(__dirname, "web");
const HTTP_PORT = 8039;

let fileServer = new staticAlias.Server(WEB_PATH, {
    // Time to cache files in seconds
    cache: 100,
    serverInfo: "Server name",
    alias: [
        {
            // If the URL contains index after '/' or nothing (excludes HTTP portion at the start)
            match: /^\/(?:index\/?)?(?:[?#].*$)?$/,
            serve: "index.html",
            force: true
        },
        {
            // If the URL contains .js
            match: /^\/js\/.+$/,
            serve: "<% absPath %>",
            force: true
        },
        {
            // If the URL contains words or numbers followed by separators, serve it and append .html
            match: /^\/(?:[\w\d]+)(?:[\/?#].*$)?$/,
            serve: function onMath(params) {
                return `${params.basename}.html`;
            }
            force: true
        },
        {
          // 404 Pages for non matching
            match: /[^]/,
            serve: "404.html"
        }
    ]
});

async function handleRequest(request, response) {
    fileServer.serve(request, response);
}

// Instantiate server
let httpServer = http.createServer(handleRequest);

httpServer.listen(HTTP_PORT);

We can then update our previous handleRequest function to check for api requests to our database.

async function handleRequest(req, res) {
  // Api Request
  if (req.url == '/get-records') {
    // Get all records from DB
    let records = await getAllRecords()

    res.writeHead(200, {
      'Content-Type': 'application/json',
      'Cache-Control': 'no-cache',
    })
    res.write(JSON.stringify(records))
    res.end()
  } else {
    // Non-api request
    fileServer.serve(req, res)
  }
}

Instead of manually routing like before using regex, we could instead use Express.js. We setup some middleware (this is a function that gets called if the incoming requests passes some criteria). Middleware is executed in order, so placement matters. We can use next() within a middleware function to execute the next middleware function other than the first.

let path = require('path')
let http = require('http')
let express = require('express')

const WEB_PATH = path.join(__dirname, 'web')
let app = express()

function defineRoutes() {
  // GET requests
  app.get('/get-records', async function (request, response) {
    let records = await getAllRecords()

    response.writeHead(200, {
      'Content-Type': 'application/json',
      'Cache-Control': 'no-cache',
    })
    response.end(JSON.stringify(records))
  })

  // Next example
  app.use(function rewriter(req, res, next) {
    if (/^\/(?:index\/?)?(?:[?#].*$)?$/.test(req.url)) {
      req.url = '/index.html'
    } else if (/^\/js\/.+$/.test(req.url)) {
      next()
      return
    } else if (/^\/(?:[\w\d]+)(?:[\/?#].*$)?$/.test(req.url)) {
      let [, basename] = req.url.match(/^\/([\w\d]+)(?:[\/?#].*$)?$/)
      req.url = `${basename}.html`
    }

    next()
  })

  //All incoming requests
  app.use(
    express.static(WEB_PATH, {
      maxAge: 100,
      setHeaders: function setHeaders(res) {
        res.setHeader('Server', 'Server name')
      },
    }),
  )
}

defineRoutes()
let httpServer = http.createServer(app)

Child Processes

We can create a child process, and then check its status.

let util = require("util");
let childProcess = require("child_process");

let child = childProcess.spawn("node", [ "file" ]);
child.on("exit", function(code) {
    console.log("Child finished", code);
})

async function main() {
    try {
        let response = await fetch(`http://localhost:{HTTP_PORT}/get-records`);

        if (response && response.ok) {
            let records = await response.json();

            if (records && records.length > 0) {
                process.excitCode = 0;

                return;
            }
        }
    }
    catch(error) {
        ...
    }

    process.exitCode = -1
}

main();

Debugging NodeJS

We can open chrome://inspect. This listens to the port on localhost. From here we can observe the server activity. We should run our programs with the --inspect flag to broadcast the port on the localhost so we can see changes from within the inspect.