mekanika / skematic

Data model & rule engine for JS objects
9 stars 0 forks source link

Skematic

Data structure and rule validation engine. Robust model schema for JS objects.

npm version Code Climate Travis

Universal, ultra fast and lightweight (4Kb!), Skematic enables you to design, format and validate data according to rules and conditions specified as simple config models, for browser and Node.js.

A basic example:

// -- Define a simple data structure
const Hero = {
  name:    {rules: {minLength: 4}, errors: 'Bad name!'},
  shouts:  {transform: val => val.trim().toUpperCase()},
  skill:   {default: 3, required: true, rules: {isNumber: true}},
  updated: {generate: Date.now}
}

// -- Format some data
Skematic.format(Hero, {shouts: '  woo   '})
// {shouts: 'WOO', skill: 3, updated: 1426937159385}

// -- Validate an object
Skematic.validate(Hero, {name: 'Zim'})
// {valid: false, errors: {name: ['Bad name!'], skill: ['Failed: required']}}

Also fully supports Typescript:

interface ISimpleHero {
  name: string
}
const SimpleHero: Skematic.Model<ISuperHero> {
  name: { required: true },
  sOmeJUNKK: {}
}

// Typescript Error:
// Object literal may only specify known properties, 
// and 'sOmeJUNKK' does not exist in type ISimpleHero

Install

npm install --save skematic

Import to your project:

// CommonJS modules
const Skematic = require('skematic')
// OR using ES6 Module imports
import Skematic from 'skematic'

To use in a browser:

<script src="https://github.com/mekanika/skematic/raw/master/node_modules/skematic/build/skematic.min.js"></script>

Compatibility Note: Skematic is written in ES6 but compiled down to ES5 and works across all modern browsers (IE9+, Chrome, Firefox, Safari evergreens). Please note that the ES5 Object.keys() method is not supported by IE7 & 8, so to use Skematic in these fossil browsers, you'll need to install es5-shim (and worship Satan :metal:).

Usage

The API surface is small by design, with two primary methods:

Design

Model configuration

Skematic provides keys to define rules and conditions for your data model. Config keys are all optional.

Format:

Validate:

Advanced:

Note: See format()'s order of execution for which formatting changes get applied in what order.

Simple examples

A basic data model:

const Hero = {
  name: {
    default: 'Genericman',
    required: true,
    rules: {maxLength: 140, minLength: 4},
    errors: {maxLength: 'Too long', minLength: 'Shorty!'}
  }
}

// Generate a record by passing null/undefined to `format(Model, null)`
Skematic.format(Hero)
// -> {name: 'Genericman'}

Skematic.validate(Hero, {name: 'Spiderman'})
// -> {valid: true, errors: null}
Skematic.validate(Hero, {name: 'Moo'})
// -> {valid: false, errors: {name: ['Shorty!']]}}

Typically you'll create a more complete data model to represent your application objects, with several fields to format and validate:

const Hero = {
  name: HeroNameField,
  skill: {default: 0}
}

Skematic.validate(Hero, {name: 'Spiderman', skill: 15})
// -> {valid: true, errors: null}
Skematic.validate(Hero, {name: 'Moo'})
// -> {valid: false, errors: {name: ['Shorty!']}

Rules

Several validation rules are built in. Custom rules are defined as functions that receive the field value and return pass/fail (true/false). Notably, 'required' is passed as a property option, rather than a rule.

Important: rules ONLY run when the value of the field is defined (i.e. NOT undefined). If a value is undefined on your data, no rules are applied. You can force a value to be provided by add the required: true flag to your model.

The other available validators are:

Custom rules can be applied by providing your own validation functions that accept a value to test and return a Boolean (pass/fail).

Note: The required rule has a special shorthand to declare it directly on the model:

const modelProp = {default: 'Boom!', required: true}

Declare rules key as follows:

const User = {
  name: {
    rules: {minLength: 5}
  }
}

Skematic.validate(User, {name: 'Zim'})
// -> {valid: false, errors: {name: ['Failed: minLength']}}

Skematic.validate(User, {name: 'Bunnylord'})
// -> {valid: true, errors: null}

Custom Rules

You can mix in Custom rules that have access to the rest of the data model via this. For example:

const User = {
  name: {
    rules: {
      // A built in validation
      minLength: 5,
      // Your own custom validator (accepts `value` to test, returns Boolean)
      // Note: MUST use `function () {}` notation to access correct `this`
      onlyFastBunnylord: function myCustomCheck (value) {
        // See us access the `speed` prop in our check:
        return value === 'Bunnylord' && this.speed > 5
      }
    }
  }
  speed: {default: 5}
}

// Wrong name
Skematic.validate(User, {name: 'Zim', speed: 10})
// -> {valid: false, errors: {name: ['Failed: minLength', 'Failed: onlyFastBunnylord']}}

// Too slow!
Skematic.validate(User, {name: 'Bunnylord', speed: 3})
// -> {valid: false, errors: {name: ['Failed: onlyFastBunnylord']}}

Skematic.validate(User, {name: 'Bunnylord', speed: 10})
// -> {vaid: true, errors: null}

Custom error messages

Custom error messages can be declared per rule name: {errors: {'$ruleName': 'Custom message'}}

Provide a default message if no specific error message exists for that rule:

{
  errors: {
    max: 'Too large',
    default: 'Validation failed'
  }
}

Usage example:

const User = {
  name: {
    rules: {minLength: 5},
    errors: {minLength: 'Name too short!'}
  }
}

// Using a value test:
Skematic.validate(User.name, 'Zim')
// -> {valid:false, errors:['Name too short!']}

// Using a keyed object value test:
Skematic.validate(User, {name:'Zim'})
// -> {valid:false, errors:{name:['Name too short!']}}

Note: You can create error messages for custom rules too. Just use the same key you used to define the custom rule. {rules: {myCustom: val => false}, errors: {myCustom: 'Always fails!'}}

Rules can be combined, and you can declare a string message on errors to apply to any and all errors:

const User = {
  name: {
    rules: {minLength: 5, maxLength: 10},
    errors: 'Name must be between 5 and 10 characters'
  }
}

Generate

Computed values - Skematic keys can generate values using functions referenced in the generate directive.

The simplest usage is to specify generate as a function:

{generate: () => Date.now()}

You may also pass generate a config object with properties:

Legend: field - {Type} default: Description

Unless instructed otherwise (via flags) generate will compute a value every time and overwrite any provided value. To preserve any provided value set preserve: true (note that undefined is treated as not set, use null to provide a no-value). To only generate a value when the key for that field is provided, set require: true. To manually run generators based on a flag provided to format, set {once: true} on the model field, (and run format(Model, data, {once: true}).

Example:

const Hero = {
  updated: {
    generate: {
      // The ops array lists fn objects or functions
      ops: [
        // A fn object specifies `fn` and `args`
        {fn: myFunc, args: []},
        // , {fn...}, etc etc
        // And here is a raw function with no args, it will be passed
        // the output of the last `fn` as its first parameter
        anotherFn
      ],
      // Optional flag: preserves a provided value
      // (default: false)
      preserve: false,
      // Optional flag: ONLY generate if provided a field on data
      // (default: false)
      require: false,
      // Optional flag: Require passing {once:true} to format to compute value
      // (default: false)
      once: true
    }
  }
};

That looks like a mouthful - but if we pass the raw functions and assume default settings for the other flags, the above collapses to:

const Hero = {
  updated: {generate: {ops: [myFunc, anotherFn], once: true}}
};

Sub-model

A property can be formatted to another model (essentially, a complex object), or array of models.

// A "post" would have comments made up of `owner_id, body`
const Post = {
  comments: { 
    model: {
      owner_id: {lock: true},
      body: {rules: {minLength: 25, }}
    }
  }
}

// Or, a simple scalar array of "tags" (an array of strings):
const Picture = {
  url: {rules: {isURL: true}},
  tags: {model: {rules: {minLength: 3}}}
}

All the model validations and checks assigned to the sub-model (comments) will be correctly cast and enforced when the parent (post) has any of its validation routines called.

primaryKey

A model can declare any one of its fields as the primary key (the id field) to be used for its data objects. This can be used in conjunction with Skematic.format() in order to modify an incoming data collection and map a pre-existing id field (say for example "_id") to the primaryKey.

This is useful for data stores that use their own id fields (eg. MongoDB uses '_id').

const propSchema = {
  prop_id: {primaryKey: true},
  name: {type: Skematic.STRING}
}

// Example default results from data store:
let data = [{_id: '512314', name: 'power'}, {_id: '519910', name: 'speed'}]

Skematic.format(propSchema, {mapIdFrom: '_id'}, data)
// -> [{prop_id: '512314', name: 'power'}, {prop_id: '519910', name: 'speed'}]

Note: Your data store might automatically use a particular field name for its identifying purposes (usually "id"). If you know you're using a datastore that defaults its id field to a given key, you can simply reuse this field name in your model. Specifying primaryKey is simply a way to force data models into using a given key.

Format

Format creates and returns a conformed data structure based on the model and input data provided.

Side-effect free, format never mutates data

Skematic.format(model [, data] [, opts])
// -> {formattedData}

Special case: Passing format no data will cause format to create blank record based on your model format(model), including defaults and generated fields. You can pass options too, as follows: format(model, null, {defaults: false})

Parameters:

Skematic.format(Hero) // create a data block
// -> {name: 'Genericman'}

Skematic.format(Hero, {name: 'Zim'})
// -> {name: 'Zim'}

// Or with options
Skematic.format(Hero, {name: 'Zim', junk: '!'}, {strict: true})
// -> {name: 'Zim'}

Format options

Format options include:

Legend: field - {Type} - default: Description

Format order of updates

Format applies these options in significant order:

  1. scopes: Checks scope match - hides field if the check fails
  2. lock: Strip locked fields (unless {unlock: true} provided)
  3. sparse: Only processes keys on the provided data (not the whole model)
  4. defaults: Apply default values
  5. generate: Compute and apply generated values
  6. transform: Run transform functions on values
  7. strip: Removes field with matching values after all other formatting
  8. mapIdFrom: Sets the id field on data to be on the 'primaryKey'

Meaning if you have an uppercase transform, it will run AFTER your generate methods, thus uppercasing whatever they produce.

Format examples:

const myModel = {
  mod_id: {primaryKey: true},
  rando: {generate: {ops: Math.random, once: true}},
  power: {default: 5},
  name: {default: 'zim', transform: val => val.toUpperCase()},
  secret: {show: 'admin'}
};

Skematic.format(myModel, {}, {once: true})
// -> {rando: 0.24123545, power: 5, name: 'ZIM'}

Skematic.format(myModel, {}) // (model, data)
// -> {power: 5, name: 'ZIM}

Skematic.format(myModel, {}, {defaults: false})
// -> {}

Skematic.format(myModel, {rando: undefined, power: 'x'}, {strip: [undefined, 'x']})
// -> {name: 'ZIM'}

Skematic.format(myModel, {name: 'Zim', secret: 'hi!'}, {scopes: ['admin']})
// -> {name: 'ZIM', secret: 'hi!'}
Skematic.format(myModel, {name: 'Zim', secret: 'hi!'}, {scopes: ['not:admin']})
// -> {name: 'ZIM'}

Skematic.format(myModel, {name: 'Gir'}, {sparse: true})
// -> {name: 'GIR'}

Skematic.format(myModel, {_id: '12345'}, {mapIdFrom: '_id'})
// -> {mod_id: '12345', power: 5, name: 'ZIM'}

Validate

Validation applies any rules specified in the model fields to the provided data and returns an object {valid, errors}:

Skematic.validate(model, data [, opts])
// -> {valid: <Boolean>, errors: {$key: [errors<String>]} | null}

Parameters:

Skematic.validate(Hero, {name: 'Zim'})

// Or with options
Skematic.validate(Hero, {name: 'Zim'}, {sparse: true})

Returns an object {valid: $boolean, errors: $object|$array|null} where the errors key may be:

Validate options include:

Legend: field - {Type} - default: Description

Development

Skematic is written in ES6+.

Developing Skemetic requires installing all dependencies:

npm install

Run the tests:

npm test

Note: Generated API docs can be found in the npm installed package under docs/index.html. Otherwise generate them using npm run docs

Benchmarks: The perf/benchmark.js is simply a check to ensure you haven't destroyed performance: npm run benchmark. Skematic runs at several tens of thousands of complex validations per second on basic hardware.

Code conventions based on Standard.

js-standard-style

Contributions

Contributions to Skematic are welcome.

License

Copyright 2017 @cayuu v2+ Released under the ISC License (ISC)