Thorin utilities

Out of the box, Thorin offers a few utilities that either abstract away some functionality or adds basic one. All utility functions are and should be placed under thorin.util

thorin.error(code, message, status, error)
Generates a custom thorin error given the error information. It is highly recommended that you wrap all your application errors using thorin.error(err) so that thorin can properly capture stack traces and disable stack trace leaking to any clients.
Ways to call:
  • thorin.error(code=string)// generic error message and status 400
  • thorin.error(code=string, message=string)// default status 400
  • thorin.error(code=string, message=string, statusCode=number)
  • thorin.error(code=string, message=string, error=Error)// attach error stack to the custom error
  • thorin.error(error=Error) // wrap a node.js error inside a thorin error with a default SERVER_ERROR code and status 500
Do's
'use strict';
const fs = require('fs');
function readFile(fpath) {
   fs.readFile(fpath, (err, content) => {
      if(err) return Promise.reject(thorin.error(err));  // wrap the default error in a thorin.error()
      return content;
   });
}
Dont's
'use strict';
const fs = require('fs');
function readFile(fpath) {
   fs.readFile(fpath, (err, content) => {
      if(err) throw err;         // NEVER throw in an async context, but gracefully handle the error.
      // do something with content
   });
}
thorin.app
The name (or type) of your application. This is useful when running multiple microservices, as this value should be used as the microservice type. By default, it will use the name of the launch file
app.js -> thorin.app = "app"
mailer.js -> thorin.app = "mailer"
thorin.id
The unique ID of your application, generated the first time you launched it. This setting persists over process restarts, it and other app options are stored under config/.thorin.
Since you might want your application to run in a cluster mode or multiple instance, the id will help you differentiate the processes
thorin.root
The root directory of the application, usually where your app.js entry file stants. This setting is used by most components, therefore it should not be manually changed.
thorin.version
When the application launched, Thorin gathers information about the project, looking into the package.json file. Changing the version key in your package.json will update your application version.
thorin.package
This holds the information contained in the package.json file, parsed and ready to be accessed.
thorin.util.randomString(length, onlyAlpha) : string
Synchronously generates a cryptographically safe random string using the specified length
  • lengthnumber the length of the string, defaults to 16
  • onlyAlphaboolean if set to true, it will only return a-zA-Z characters.
thorin.util.extend()
A utility function that will extend the first object with the other sources and recursively merge them.
  • argumentsobject called with as many objects as needed in the arguments scope.
This would be equivalent with Object.assign. A short example of the extend function:
'use strict';
const myObj = thorin.util.extend({"v1": "one"}, {"v2": "two"});
// myObj is now {"v1": "one", "v2": "two"} with a new object reference.
thorin.util.readDirectory(path, opt) : array(string)
Synchronously and recursively reads the contents of a directory
  • path stringthe absolute path of the directory
  • optobject additional options to use (see below)
Available options:
  • ext - only include files with the specified extension, OR
  • dirs - if set to true, return only directories
  • modules - if set to true, will go into node_modules folders, by default it excludes them.
  • relative - if set to true, all resulting paths will be converted to the relative path of the search path, excluding the first slash (eg: /home/ec2-user/myapp/myfolder/app.js => myfolder/app.js)
thorin.util.isDirectory(path) : boolean
Synchronously checks if the given path is a directory or not
  • path string the absolute path of the directory to check.
thorin.util.isFile(path) : boolean
Synchronously checks if the given path is a file or not
  • path string the absolute path of the file to check.
thorin.util.sha1(text) : string
Hashes the given text using SHA-1
  • text string the text to hash
thorin.util.sha2(text, count) : string
Hashes the given text using SHA-2, count times
  • text string the text to hash
  • countnumber the number of rounds to apply the hash, defaults to 1.
thorin.util.hmac(text, secret, alg) : string
Creates an HMAC with the given secret and hashes the given text
  • text string the text to hash
  • secret string the secret key to initiate the HMAC with
  • algstring the HMAC algorithm to use, defaults to sha256.
thorin.util.encrypt(data, key, iv) : string
Encrypts the given data with Node's native crypto module using AES-256-CBC. If the decryption process fails, returns false
  • datastring | buffer the data to encrypt
  • keystring the 32-character key to use for encryption
  • ivboolean | buffer | string if set to true, generate an IV on the spot. If string or buffer, use the provided one.
The encoding used for both the ciphertext and IV (if any) is hex. When using with an IV, the ciphertext pattern is:
{32-char hex-encoded IV}${hex-encoded ciphertext}
Notice the dollar $ sign between the IV and the ciphertext. We use it to differentiate between IV and non-IV encryptions, when decrypting the ciphertext.
thorin.util.decrypt(ciphertext, key, iv) : string
Tries to decrypt the ciphertext using the given key and (optional) IV with Node's native crypto module, using AES-256-CBC. If the decryption process fails, returns false
  • ciphertextstringthe generated ciphertext of a previous encryption
  • keystring the 32-char key used for decryption
  • ivstring If the ciphertext does not contain an IV (see above), it will look for it in this argument. If none is specified, initiates the decipher with no IV.
thorin.util.compare(a,b) : boolean
Performs a safe byte comparison on the two strings. This is useful to mitigate timing attacks.
  • a, bstring the two strings to compare.
thorin.util.downloadFile(url, done)
Downloads a given static css or js resource from the given url and calls back with its string content.
  • url string the URL to download the asset from
  • donefunction the callback to call when download is ready.
thorin.util.Event
A small wrapper over Node's EventEmitter that exposes the destroy() function, removing all registered listeners. This is to prevent memory leaks.
The Thorin Fetcher

Thorin also exposes a small wrapper over fetch's implementation that facilitates communication between other thorin microservices. Since the de-facto transport layer used by most microservices is the thorin-transport-http, we can leverage the /dispatch endpoint to handle action triggering between microservices.

One of thorin's fetch wrappers is exposed under thorin.fetch that wraps the default node-fetch module in a error-checker result-parser way.

thorin.fetch(url, opt, done)
Performs a fetch on the given url using the given node-fetch options. If a callback is specified, use it otherwise return a promise. The fetch's result is considered a success if the statusCode is 2xx and the its content-type is set to application/json and the result is a valid JSON object, OR the result is a plain text.
  • urlstring the URL to perform the request to
  • optobject the options to send to node-fetch. For a full list, click here.
  • donefunction if specified, use the callback for passing the response. Otherwise, use promises.
You can quickly set an Authorization: Bearer {code} header by using opt.authorization: "yourToken".

A more complex fetcher can be configured by using thorin.fetcher. It essentially configures and caches the way your application communicates with another thorin microservice.

thorin.fetcher(name, url, opt)
Creates and caches a new fetcher instance using the given options or returns a previously registered one. If the fetcher's name is omitted, it will not be cached.
  • name string the fetcher name used to cache.
  • urlstring the full url containing the /dispatch path of the target microservice.
  • optobject additional options to use with node-fetch (see note above)
A fetcher instance will contain a dispatch(action, payload) function that will perform a POST request to the target's /dispatch endpoint, using the given action and payload. Returns a promise.
Ways to call:
  • thorin.fetcher(url=string, opt=object) // creates a fetcher but does not cache it
  • thorin.fetcher(name=string, url=string, opt=object // creates a fetcher and caches it using the given name
  • thorin.fetcher(name=string) // returns a previously cached fetcher
Example:
'use strict';
const mailer = thorin.fetcher('mailer', 'http://localhost:12301/dispatch', {authorization: 'SOME_TOKEN'});
mailer
   .dispatch('my.action', {some: "payload"})
   .then((res) => {
      log.info('Mailer responded with', res);
   })
   .catch((e) => {});

// Access a previously defined fetcher.
const mailerApi = thorin.fetcher('mailer');
Additional utilities

Thorin internally makes use of additional utility libraries. Since we do not want to duplicate dependencies, they will be exported under thorin.util.{library}

thorin.util.uuid
Expose the internal dependency of node-uuid
thorin.util.fs
Expose the internal dependency of fs-extra
thorin.util.async
Expose the internal dependency of async
thorin.util.fetch
Expose the internal dependency of node-fetch
thorin.loadPath(paths, ignoreInitialized, args)
Recursively requires the given file paths or folders. It is useful when you want to require all the files inside a folder, recursively.
  • pathsstring | array[string]A path or an array of paths to require. If the path is not absolute, thorin.root will be used to prepend it.
  • ignoreInitializedboolean By default, it will wait until the thorin application will start initializing. Defaults to false (see below)
  • The paths that are to be loaded can be either directory or file paths. If a path is a directory, thorin will try to recursively load all the .js files inside that directory.
  • If the path is not absolute, the full path will be thorin.root + '/' + path (use the application folder as the root path).
  • When calling loadPath(path, true), the require() call will wait untill you call thorin's run() function to boot up the application and wait untill all components had their init() function called.
  • If the exported variable of a required file is a function, thorin will call it, passing any additional arguments that you've specified in the loadPaths() call, excluding the paths and ignoreInitialized arguments.
'use strict';
const thorin = require('thorin');
// File: app.js
thorin.loadPath('app/lib', thorin, "Hello"); // pass thorin and true as a variable for the exported functions.

'use strict';
// File: app/lib/file.js
module.exports = (thorin, word) => {
   log.info('Required with word:', word);
   // do stuff.
}
thorin.series(items, fn, stopOnError)
A utility function that will perform a series() on the array of functions provided. The items array should be an array that contains functions. Each function will be called and should return
- promises, in which case it will wait for them to resolve
- undefined, in which case we consider them successful.
  • itemsarray of functions an array of functions that are waiting to be called in a serial model.
  • fnif specified, use it as the callback, otherwise return a promise
  • stopOnErrorif set to false, we will ignore all errors or promise rejections. By default, we stop at the first occurance of an error.
Ways to call:
  • thorin.series(items=array,stopOnError=false) // work with promises and won't stop on first error.
  • thorin.series(items=array, onComplete=function, stopOnError=false) // work with callback and won't stop on the first error.
  • thorin.series(items=array, onComplete=function) // works with callback, stops at first error
  • thorin.series(items=array) // works with promises, stops at first error.
'use strict';
const store = thorin.store('sql');
const calls = [];
let appObj;
calls.push(() => {
   const Account = store.model('account');
   return Account.build({email: 'john@doe.com'});
});
if(thorin.env === 'production') {
   calls.push(() => {
      log.info('Account created');
      return Application.find({where: { id: 1 }).then((obj) => appObj = obj);
   });
}
calls.push(() => {
   if(!appObj) return;
   return appObj.update({is_active: true});
});

thorin.series(calls, (e) => {
   log.info('All done!');
});
Do you have a question or is something missing?

You can always create a new issue on GitHub or contact one of the core founders by chat.