Skip to content

mobily-enterprises/simpleschema

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SimpleSchema TWO

SimpleSchema is a simple library to validate objects and cast their attributes according to their (schema) types.

Main features:

  • It's very easy to use and extend (adding both data types and parameters)
  • It's tailored for req.body, built for casting simple (not nested) data strucures
  • It down-to-earth ES2015 code
  • Fully unit-tested (note: no longer true after rewrite, but it will be soon)

Brief introduction

Here is SimpleSchema in a nutshell: var Schema = require( 'simpleschema' );

personSchema = new Schema({
  name: { type: 'string', trim: 20 },
  age:  { type: 'number', default: 30, max: 140 },
  rank: { type: 'number', default: 99, max: 99 },
});  

In a normal node/Express application, you would simply use the validate() method of personSchema against req.body:

// Definition of a standard callback

function formSubmit( req, res, next ){

  var { validatedObject, errors } = self.schema.validate(req.body)

  if( errors.length) {
    // Do what you normally do when there is an error,
    // ...

    // For example, make up a new error and go to the next route
    // The error object will include an `errors` attribute with the validation errors
    var e = new Error()
    e.errors = errors
    return next(e)
  }

  // Cleanup (that is, delete) `rank` which must not be stored on the DB
  var dbReady = personSchema.cleanup( validatedObject, 'doNotSave' )
})

This ensures that all values are cast appropriately (everything in req.body comes as a string, whereas you will want age and rank as proper Javascript numbers).

Note that in this field:

  rank: { type: 'number', default: 99, max: 99, doNotSave: true },
  • type is the field type. It means that when running personSchema.validate(), rank will be cast as a number
  • default, max are the "field parameters".

The schema description: all features

Here is a schema which covers every single feature in terms of types and parameters (parameters will not be repeated):

// If there is an error, the validator function will need to return a string describing it.
// otherwise, return nothing.
var fieldValidatorFunc =  function( obj, value, fieldName ){
  if( value == 130 ) return 'Age cannot be 130';
  return;
};

complexSchema = new Schema({
  id:      { type: 'id' },
  name:    { type: 'string', default: 'SOMETHING', uppercase: true, trim: 4, required: true, notEmpty: true },
  surname: { type: 'string', lowercase: true },
  age:     { type: 'number', default: 15, min: 0, max: 150, validator: fieldValidatorFunc },
  date:    { type: 'date', emptyAsNull: true},
  list:    { type: 'array', canBeNull: true },
  various: { type: 'serialize', required: false },
})

Note:

  • Casting to the field's type (depending on type) always happens first; parameters (min, max, lowercase, etc.) are applied afterwards
  • If casting to its type fails, no parameters for that field will be applied (and errors will have the casting error on that field)
  • The order of parameters matters. Parameters are processed in the order they are encountered. If you have { default: 'something', uppercase: true }, the result will be SOMETHING.
  • the serialize type will convert an object into a string. You need to use the option { deserialize: true } when validating if you want to do the opposite.
  • min, max on strings will check the string length; on numbers will check number value
  • uppercase, lowercase, trim will only apply to strings
  • notEmpty will fail if the object's corresponding attribute was v == '' (note the weak ==) and will never fail for arrays
  • required will fail if the object's corresponding attribute (before casting) was undefined and will never fail for arrays;
  • emptyAsNull will make sure that values that cast to an empty string are converted to null.
  • canBeNull will allow values to be stored as null, bypassing casting and parameters.
  • string type checks parameter noTrim (it will not trim if there)
  • boolean type checks parameters stringFalseWhen and stringTrueWhen so that the strings false and true can be seen as false and true respectively
  • If fieldValidatorFunc returns a string, then an error will be added for that field. Note that this function is synchronous

Note: required, emptyAsNull and canBeNull are the only parameters implemeted directly into the validate function itself, which is SimpleSchema's core. Everything else is

Validating against a schema

Validation happens with the schema.validate() function:

var { validatedObject, errors } = complexSchema.validate(object, {})

The validate() function takes the following parameters:

  • The object to validate
  • An optional options object with extra options

Here is an example of basic usage:

let p = { name: 'TOnyName', surname: 'MOBILY', age: '37', id: 3424234424, date: '2013-10-10', list: [ 'one', 'two', 'three' ], various: { a: 10, b: 20 } }

let { validatedObject, errors } = complexSchema.validate(p)

validatedObject will be:

{ name: 'TONY',
  surname: 'mobily',
  age: 37,
  id: 3424234424,
  date: Thu Oct 10 2013 08:00:00 GMT+0800 (WST),
  list: [ 'one', 'two', 'three' ] },
  various: '{"a":10,"b":20}'
}

And errors will be empty. Note that name is uppercase and trimmed to 4, surname is lowercase, age is now a proper Javascript number, date is a proper date.

The returned errors array

The errors variable is an array of objects; each element contains field (the field that had the error) and message (the error message for that field). For example:

[
  { field: 'age', message: 'Age cannot be 130' },
  { field: 'name', message: 'Name not valid' }
]

The same field can potentially have more than one error message attached to it.

The options object

The second parameter of schema.validate() is an (optional) options object. Possible values are:

onlyObjectValues

This option allows you to apply schema.validate() only to the fields that are actually defined in the object, regardless of what was required and what wasn't. This allows you to run schema.validate() against partial objects. For example:

p = {
  name: 'MERCMOBILY',
}

var { validatedObject, errors } = complexSchema.validate(p)

validatedObject will be:

{ name: 'MERC' }

Note that only what "was there" was processed (it was cast and had parameters assigned).

skipFields

The option skipFields is used when you want to skip validation completely for specific fields.

p = {
  name: 'TOny',
  surname: 'MOBILY',
  age: '37',
  id: 3424234424,
  date: '2013-10-10',
  list: [ 'one', 'two', 'three' ]
}

let { validatedObject, errors } = complexSchema.validate(p, { skipFields: [ 'age' ]})

validatedObject will be (note that '37' is still a string):

{ name: 'TONY',
  surname: 'mobily',
  age: '37',
  id: 3424234424,
  date: Thu Oct 10 2013 08:00:00 GMT+0800 (WST),
  list: [ 'one', 'two', 'three' ] },
}

skipParams

The option skipParams is used when you want to decide which parameters you want to skip for which fields.

p = {
  name: 'Chiara',
  surname: 'MOBILY',
  age: '37',
  id: 3424234424,
  date: '2013-10-10',
  list: [ 'one', 'two', 'three' ]
}

let { validatedObject, errors } = complexSchema.validate(p, { skipParams: { name: [ 'uppercase', 'trim' ] } }

validatedObject will be:

{ name: 'Chiara',
  surname: 'mobily',
  age: 37,
  id: 3424234424,
  date: Thu Oct 10 2013 08:00:00 GMT+0800 (WST),
  list: [ 'one', 'two', 'three' ] },
}

Note that name is still unchanged: it didn't get lowercased, nor trimmed.

emptyAsNull

If this parameter is true, then values that would be cast to empty strings will be cast to null. This is to be used if you prefer to store null on your database rather than empty values.

canBeNull

If this parameter is true, then null will be accepted as value, and casting and validation will be completely skipped.

deserialize

In some cases, you might want serialize to work the other way around: you want to convert a JSON string into an object. This is common if, for example, you want to 1) Receive the data via req.body 2) Store the data after schema.validate() (any serialize field will be serialized) 3) Later on, fetch the data from the database 4) Validate that data against the same schema (in which case, you will use the option { deserialize: true }).

This option, if set to true, will make serialize work the opposite way: data will be converted back from string to Javascript Objects.

Per field validation

In the schema, you can define a field as follows:

age:  { type: 'number', default: 15, min: 10, max: 40, validator: fieldValidatorFunc },

Where fieldValidatorFunc is:

var fieldValidatorFunc =  function( obj, value, fieldName ){
  if( value === 130 ) return 'Age cannot be 130';
  return;
};

In fieldValidatorFunc, the this variable is the schema object. If the function returns a string, that will be the error. If it returns nothing, then validation went through.

Note that this validation is synchronous. It's meant to be used to check field sanity.

Extending the class

The basic schema is there to be extended. You can do so by creating javascript mixins:

var ExtrasMixin = (superclass) => class extends superclass {

  floatStringType (p) {
    if (typeof (p.value) === 'undefined') return 0.0

    // If Number() returns NaN, fail
    var r = Number(p.value)
    if (isNaN(r)) {
      throw this._typeError(p.fieldName) // ._
    }
    // Return cast value
    return r.toFixed(2)
  }

  capitalizeParam (p) {
    if (typeof (p.value) !== 'string') return
    return  p[0].toUpperCase() + p.slice(1)
  }
}

Just create a new schema class that "mixes" the mixin:

var ImprovedSchema = ExtraMixin(Schema)

And use ImprovedSchema as the constructor for your schema object.

Now in your schema you can have entries like this (note that 'capitalize' as a parameter and 'floatString' as type):

age:     { type: 'string', capitalize: true },
price:   { type: 'floatString' },

The next 2 sections will detail how to write code for new types and new parameters.

Extending types

Types are defined by casting functions. When validate() encounters:

surname: { type: 'string', lowercase: true },

It looks into the schema for a function called stringType. It finds it, so it runs:

stringType (p) {
  // Undefined: return '';
  if (typeof (p.value) === 'undefined') return ''
  if (p.value === null) return ''

  // No toString() available: failing to cast
  if (typeof (p.value.toString) === 'undefined') {
    throw this._typeError(p.fieldName) // ._
  }

  // Return cast value
  return p.value.toString()
}

Whatever is returned by this function will be used as the new value for the field, before applying parameters. If there is a problem, the _typeError method is called and an error will be thrown.

The parameters passed to the function are:

  • definition. The full definition for that field. For example, { type: 'string', lowercase: true }
  • value. The value of the record for that field
  • fieldName. The field's name
  • object. The object that is being worked on. At this stage, it might be partially validated.
  • objectBeforeCast. The original object that was passed for validation.
  • options: Options passed to the validate() function

Extending parameters

Parameters are based on the same principle. So, when validate() encounters:

surname: { type: 'string', lowercase: true },

it will look for this.lowercaseParam(), which is:

lowercaseParam (p) {
  if (typeof (p.value) !== 'string') return
  return p.value.toLowerCase()
}

Or when validate() encounters:

  age:     { type: 'number', default: 15, min: 0, max: 150, validator: fieldValidatorFunc },

it will look for this.maxParam, which is:

    maxParam (p) {
      if (typeof p.value === 'undefined') return
      if (p.definition.type === 'number' && typeof p.value === 'number' && Number(p.value) > p.parameterValue) {
        throw this._paramError(p.fieldName, "Field's value is too high")
      }

      if (p.definition.type === 'string' && p.value.toString && p.value.toString().length > p.parameterValue) {
        throw this._paramError(p.fieldName, 'Field is too low')
      }
    }

Whatever is returned by this function will be used as the new value for the field, before applying parameters. If nothing is returned, then the object's value isn't changed (this is useful for parameters like min and max, which are meant to generate errors more than modifying afield)

If there is an error, param functions can use this._paramError() which will throw an error.

The parameters passed to param functions are:

  • definition. The full definition for that field. For example, { type: 'string', lowercase: true }
  • value. The value of the record for that field
  • fieldName. The field's name
  • object. The object that is being worked on. At this stage, it might be partially validated.
  • objectBeforeCast. The original object that was passed for validation.
  • valueBeforeCast. The value of the field before casting.
  • parameterName. The parameter's name (e.g. max)
  • parameterValue. The parameter's value (e.g. 10)
  • options: Options passed to the validate() function

This is identical to values passed to Type methods, plyus three extra properties (valueBeforeCast, parameterName and parameterValue)

API description

This is the full list of functions available with this module:

constructor(schemaObject)

Make up the schema object, assigning the this.structure field.

xxxType( p )

Extending function that will define the type xxx. It will allow you to use a new type in your schema: field1: { type: 'xxx' }

xxxParam( p )

Extending function to define the parameter xxx. It will allow you to use a new parameter in your schema: field1: { type: 'number', xxx: 10 }. Note that a parameter can apply to any type -- it's up to the parameter helper function to decide what to do.

validate( object, options)

Applies schema casting and parameters to the passed object.

Parameters:

  • object. The object to cast and check
  • options. Options received by all param and casting functions. Note that the options object is passed to all Type and Param functions. Option properties used by the stock SimpleSchema class are:
  • onlyObjectValues (boolean). Used by the class itself.
  • deserialize (boolean). Used by the serialize type.

cleanup()

Clean up fields with a specific parameter defined.

Parameters:

  • object The object to cleanup
  • parameterName The name of the parameter that will be hunted down. Any field that in the schema structure has thar parameter fill be deleted from object

About

The simplest, most extendible schema class you will ever come across

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published