node package manager

tsa

tsa

Guard your REST API with a bit of fascism.

TSA is a node.js library designed to take JSON input and:

  • filter it against a whitelist
  • validate it
  • transform it
  • provide default values

It has been designed with usage in an Express-based JSON REST API in mind, and allows you to easily pass it into your route as middleware.

Table of Contents

Installation

$ npm install tsa

Usage

Using TSA Directly

Create a guard:

var tsa = require('tsa');
var guard = tsa({
    property1: tsa.required()
  , property2: tsa.optional()
  , property3: tsa.default('blah')
});

Validate input against guard:

var input = {
    property1: 'foo'
  , property4: 'bar'
};
guard().frisk(input, function(err, result){
  // err === null 
  // result.property1 === 'foo' 
  // result.property2 === undefined 
  // result.property3 === 'blah' 
  // result.property4 === undefined 
});

Using TSA via Express Middleware

Create a guard:

var tsa = require('tsa');
var guard = tsa({
    property1: tsa.required()
  , property2: tsa.optional()
});

Ensure you're using express's body parser:

app.use(express.bodyParser());

Add that guard's middleware to your route:

app.post('/foo', guard().middleware(), function(req, res){
  // req.body is the whitelisted, validated, transformed version of the input from req.body 
});
 
app.error(function(err, req, res, next){
  // err is an array of errors generated by the guard 
});

Alternatively you can handle the errors on a per-route basis instead of globally:

app.post('/foo', guard().middleware(function(err, req, res, next){
  // return a 400, show an error page, ignore by calling next, whatever 
}), function(req, res){
  // req.body is the whitelisted, validated, transformed version of the input from req.body 
});

Creating Guards

Nested Guards

var address = tsa({
    street1: tsa.required()
  , street2: tsa.optional()
});
var person = tsa({
    name: tsa.required()
  , address: address()
});

Nested guards can also be created inline:

var person = tsa({
    name: tsa.required()
  , address: tsa({
      street1: tsa.required()
    , street2: tsa.optional()
  })()
});

You can validate/transform/etc nested guards either at the definition level, or the usage level:

// example of adding validations to guard definition 
var address = tsa({
    street1: tsa.required()
  , street2: tsa.optional()
}, {validate: someValidationFunction});
// example of adding validations to guard usage 
var person = tsa({
    name: tsa.required()
  , address: address({validate: aDifferentValidationFunction})
});

Required Properties

var guard = tsa({
  property1: tsa.property({ required: true }) // or: tsa.required() 
});
var input = {};
guard().frisk(input, function(err, result){
  // err === instanceof Array 
  // err[0] === {key: 'property1', error: 'Required property property1 not supplied.'} 
});

You can provide a custom error message like so:

var guard = tsa({
    example1: tsa.property({ required: 'fail!' })
  , example2: tsa.required('fail!')
});

Optional Properties

var guard = tsa({
  property1: tsa.property() // or: tsa.optional() 
});
var input = {};
guard().frisk(input, function(err, result){
  // err === null 
  // result === {} 
});

Whitelisting

var guard = tsa({
  property1: tsa.required()
});
var input = {
    property1: 'foo'
  , property2: 'bar'
};
guard().frisk(input, function(err, result){
  // result.property1 === 'foo' 
  // result has no property2 key 
});

Default Values

var guard = tsa({
  foo: tsa.property({ default: 'bar' }) // or: tsa.default('bar') 
});
var input = {};
guard().frisk(input, function(err, result){
  // err === null 
  // result.foo === 'bar' 
});

Optionally, the default value can be a function which will be executed by tsa:

var now = function(){
  return new Date();
};
var guard = tsa({
  foo: tsa.property({ default: now }) // or: tsa.default(now) 
});
var input = {};
guard().frisk(input, function(err, result){
  // err === null 
  // result.foo === a Date object 
});

Built-In Validations

TSA ships with a few validations built-in. Here are some examples:

var guard = tsa({
   foo: tsa.require({ validate: tsa.validate.boolean() })
 , bar: tsa.require({ validate: tsa.validate.boolean('The value "%1" is not a boolean.') }) // <- custom error message 
 , baz: tsa.require({ validate: tsa.validate.true() })
 , boo: tsa.require({ validate: tsa.validate.false() })
});
var guard = tsa({
   foo: tsa.require({ validate: tsa.validate.numeric() })
 , foo2: tsa.require({ validate: tsa.validate.numeric('fail!') }) // <- custom error message 
 , bar: tsa.require({ validate: tsa.validate.range(0, 10) })
 , bar2: tsa.require({ validate: tsa.validate.range(0, 10, {
       invalid: 'custom error message'
     , below: 'custom error message'
     , above: 'custom error message'
   }) })
 , baz: tsa.require({ validate: tsa.validate.min(0) })
 , baz2: tsa.require({ validate: tsa.validate.min(0, {
       invalid: 'custom error message'
     , below: 'custom error message'
   }) })
 , boo: tsa.require({ validate: tsa.validate.max(10) })
 , boo2: tsa.require({ validate: tsa.validate.max(10,
       invalid: 'custom error message'
     , above: 'custom error message'
   }) })
});
var guard = tsa({
   foo: tsa.require({ validate: tsa.validate.regex(/^bar$/g) })
 , foo: tsa.require({ validate: tsa.validate.regex(/^bar$/g, 'fail!') }) // <- custom error message 
});

Custom Validations

var mustBeUpper = function(input, cb){
  if(input.toUpperCase() === input){
    cb(); // yes, this is uppercase 
  }else{
    cb('not uppercase!'); // oh noes! 
  }
};
var guard = tsa({
  foo: tsa.property({ validate: mustBeUpper }) // or: tsa.validate(mustBeUpper) 
});
var input = { foo: 'bar' };
guard().frisk(input, function(err, result){
  // err[0] === {key: 'foo', error: 'not uppercase!'} 
  // result === null 
});

Your custom validations can return multiple errors, if necessary:

var myValidationFunction = function(input, cb){
  if(...){
    cb(); // passed! 
  }else{
    cb(['error message 1', 'error message 2']); // failed... 
  }
};

Transformations

var toUpper = function(input, cb){
  cb(null, input.toUpperCase());
};
var guard = tsa({
  foo: tsa.property({ transform: toUpper }) // or: tsa.transform(toUpper) 
});
var input = { foo: 'bar' };
guard().frisk(input, function(err, result){
  // err === null 
  // result.foo === 'BAR' 
});

Sanitization

Sanitizing a property runs a validation function against it, but rather than failing the guard if an error is reported that property is simply thrown away in the case of an error:

var mustBeUpper = function(input, cb){
  if(input.toUpperCase() === input){
    cb(); // yes, this is uppercase 
  }else{
    cb('not uppercase!'); // oh noes! 
  }
};
var guard = tsa({
    foo: tsa.property({ sanitize: mustBeUpper }) // or: tsa.sanitize(mustBeUpper) 
  , fizz: tsa.sanitize(mustBeUpper)
});
var input = { foo: 'bar', fizz: 'BANG' };
guard().frisk(input, function(err, result){
  // err === null 
  // result.foo === undefined 
  // result.fizz === 'BANG' 
});

Note that you can run TSA's built-in validations through sanitize:

var guard = tsa({
  foo: tsa.sanitize(tsa.validate.regex(/^bar$/g))
});

Rename Properties

var guard = tsa({
  foo: tsa.property({ rename: 'bar' }) // or: tsa.rename('bar') 
});
var input = { foo: 'blah' };
guard().frisk(input, function(err, result){
  // result.foo === undefined 
  // result.bar === 'blah' 
});

Combinations

You can combine any and all of the above like so:

var toUpper = function(input, cb){
  cb(null, input.toUpperCase());
};
var guard = tsa({
  foo: tsa.property({ required: true, transform: toUpper })
  // or: tsa.required({ transform: toUpper }) 
  // or: tsa.transform(toUpper, {required: true}) 
});
var input = { foo: 'bar' };
guard().frisk(input, function(err, result){
  // err === null 
  // result.foo === 'BAR' 
});

Error Handling

Errors for nested structures are returned like so:

[
    {key: 'first', error: 'Required property not provided.'}
  , {key: 'address', error: [
        {key: 'street1', error: 'Required property not provided.'}
      , {key: 'zip', error: 'Required property not provided.'}
    ]}
]

While this is a very structured format, it isn't always the easiest for doing things like highlighting form fields that have errors. In those situations you can pass the error structure into the tsa.flattenErrors method to get back something like this:

[
    {key: 'first', error: 'Required property not provided.'}
  , {key: 'address[street1]', error: 'Required property not provided.'}
  , {key: 'address[zip]', error: 'Required property not provided.'}
]

Passing {hash: true} into tsa.flattenErrors as the second argument results in:

{
    'first': ['Required property not provided.']
  , 'address[street1]': ['Required property not provided.']
  , 'address[zip]': ['Required property not provided.']
}

Test

Run tests via mocha:

$ npm install -g mocha
$ git clone git://github.com/TroyGoode/node-tsa.git tsa
$ cd tsa/
$ npm install
$ mocha

Run example web app:

$ git clone git://github.com/TroyGoode/node-tsa.git tsa
$ cd tsa/
$ npm install
$ cd example/
$ npm install
$ npm start
$ open http://localhost:3000

License

MIT License

Author

Troy Goode (troygoode@gmail.com)