/VeryModel

A JavaScript model system for validation, creation, and editing of models.

Primary LanguageJavaScriptMIT LicenseMIT

the VeryModel of a modern major general

npm i verymodel

VeryModel

A JavaScript model system for validation, creation, and editing of models.

I wrote this because the robust model systems that I found were tightly integrated with frameworks, or only served to be useful for validation.
VeryModel is not tied to a framework, and it implements a full purpose Model system.

OK, But What Is It?

Models are useful for managing the lifecycle of an object. They are commonly used for framework ORMs (Object Relational Managers) and the M in MVC (Model-View-Controller) patterns like Backbone.js.

Models can also be extended with functionality for interacting with databases and network/HTTP APIs, making them little SDKs specific to each type of data you deal with.

Quick Example

verymodel.Model is a constructor (must be called with new) that takes a definition (object of fields and parameters).

var verymodel = require('verymodel');

// setup your factory
var SomeModelFactory = new verymodel.Model({
    //definition here
    some_field: {
        // field parameters (see Definition Spec below)
    },
    some_other_field: {
        // field parameters (see Definition Spec blow)
    },
}); 

// create an instance of a model
var model_instance = SomeModelFactory.create({
    //initial model data here
}); 

The resulting object is a factory that produces model instances based on the defintion with the .create(values) method.

Model instances are working instances of your object data. They use property setters/getters to interface with your data, and are not simple JSON style objects.

Compatibility Changes with v2

The main difference between v3 and v2 is that the definition validator must now be a Joi validator.

As such, "required" is no longer a necessary field, depends is now an array, and several undocumented features were removed.

Index

Adding functionality

Both Model Factories and Model Instances can be extended to add parameters and functions, typically used for database interactions like load and save() or HTTP REST calls like list(), get(), post(), put(), delete().

Functions that load data should be added onto the Factory like load, list, getByName, etc.

//these functions can be named anything, do anything, and have any parameters.
//extending the factory with new functions is useful for dealing with the model BEFORE it contains any data (like loading/getting)
SomeModelFactory.load = function (id, callback) { //most IO in Node.js is async, so here's an callback example
    db.get(id, function (err, result) {
        callback(err, this.create(result));
    });
}

SomeModelFactory.list = function (offset, count, callback) {
    db.select("SELECT * FROM SomeTable LIMIT %d %d", offset, count, function (err, results) {
        var model_instances = [];
        if (!err) {
            results.
        }
    });
}

Functions that you want to use on Model Instances like save, delete is extended with extendModel.

SomeModelFactory.extendModel({
    save: function (callback) {
        db.set(this.key, this.toJSON(), callback);
    },
    delete: function (callback) {
        db.del(this.key, callback);
    }
});

Extended Example

var generaldef = {
    name: {
        model: {
            first: {validator: joi.string().alphanum().min(2).max(25)},
            last: {validator: joi.string().alphanum().min(3).max(25)},
            title: {depends: ['last'],
            full: {derive: function (name) {
                return (typeof name.title !== 'undefined' ? name.title + ' ' : '') + (typeof name.first !== 'undefined' ? name.first + ' ': '') + name.last;
                }
            }
        }
    },
    knowledge: {collection: {
            name: {},
            category: {validate: joi.any().valid(['vegetable', 'animal', 'mineral'])}
        }
    },
    rank: {
        validate: joi.any().valid(['Private', 'Corpral', 'Major', 'General', 'Major-General']),
        default: 'Major-General'
    }
};

This class interprets defintions and spawns models from create.

Initialize with a definition.

var MajorGeneral = new verymodel.Model(generaldef);
var stanley = MajorGeneral.create({
    name: {title: 'Major-General', last: 'Stanley'},
    rank: 'Major-General',
    knowledge: [{name: 'animalculous', category: 'animal'}, {name: 'calculus', category: 'mathmatical'}]
});
var errors = stanley.doValidate();
console.log(errors);

Output:

[ 'knowledge[1].category: Unexpected value or invalid argument' ]

Turns out he knows more than just animals, vegetables, minerals.

stanley.knowledge[1].category = 'vegetable';

That ought to do it.

var errors = stanley.doValidate();
console.log(errors);

Output:

[]

Let's see what our object looks like:

console.log(stanley.toJSON());

Output:

{ name:
   { last: 'Stanley',
     title: 'Major-General',
     full: 'Major-General Stanley' },
  knowledge:
   [ { name: 'animalculous', category: 'animal' },
     { name: 'calculus', category: 'vegetable' } ],
  rank: 'Major-General' }

Noticed that the derived field, name.full was populated.

##Field Definitions

type

A string which references a built in type. Built in types include string, array, integer, numeric, enum, boolean. Strings and arrays may have min and max values, both for validation, and max will truncate the results when saving or on toJSON. Enums may include values, an array (and eventually a ECMAScript 6 set).

You can override any of the definition fields of a specified type. Validate, processIn, processOut, and onSet will use both the built-in and your override. The others will replace the definition field.

type does not need to be set at all. In fact, {} is a perfectly valid definition.

Example:

{field: {type: 'string', max: 140}}

model

The model parameter defines a submodel. It can be an object of field definitions, a VeryModel Factory Instance, or the string matching the name of a VeryModel factory described in it's options.

collection

Like sub models, collections may be a string name of a model, model factory, or model definition object in order to define an array of models.

validate

The validate field takes a Joi validator and should determine whether that value is acceptable or not. It's run during doValidate().

Example:

new verymodel.Model({field: { validate: Joi.string().max(2) }});

processIn

processIn is a function that is passed a value on loading from the database, create, or loadData. It should return a value.

This function is often paired with processOut in order to make an interactive object when in model form, and a serialized form when converted.

processIn does not handle the case of direct assignment like modelinst.field = 'cheese';. Use onSet for this case.

Example:

new verymodel.Model({someDateField: {
    processIn: function (value) {
        return moment(value);
    },
})

processOut

processOut is a function that takes a value and returns a value, just like processIn, but is typically used to serialize the value for storage. It runs on toJSON().

Example:

new verymodel.Model({someDateField: {
    processOut: function (value) {
        return value.format(); //turn moment into string
    },
})

processors

processors is an object that contains functions that takes a value and returns a value, just like processIn and processOut, but only run when you call create or toJSON with the option of processors with an array item that matches the key to this function.

Example:

var model = new verymodel.Model({name: {
    processors: {
        customProcessor: function (value) {
            return value + '!'; //turn moment into string
        }
    }
});

model.create({someDateField: 'Fritzy'}, {processors: ['customProcessor']});
console.log(model.name); // Fritzy!

onSet

onSet is just like processIn, except that it only runs on direct assignment. It's a function that takes a value and returns a value.

Example:

new verymodel.Model({someDateField: {
    processIn: function (value) {
        return moment(value);
    },
    onSet: function (value) {
        if (moment.isMoment(value)) {
            return value;
        } else {
            return moment(value);
        }
    },
    processOut: function (value) {
        return value.format();
    },
})

derive

derive is a function that returns a value whenever the field is accessed (which can be quite frequent). The this context, is the current model instance, so you can access other fields.

Example:

new verymodel.Model({
    firstName: {type: 'string'},
    lastName: {type: 'string'},
    fullName: {
        type: 'string',
        derive: function () {
            return [this.firstName, this.lastName].join(" ");
        },
    }
});

❗ Warning! DO NOT REFERENCE THE DERIVE FIELD WITHIN ITS DERIVE FUNCTION! You will cause an infinite recursion. This is bad and will crash your program.


default

default may be a value or a function. In function form, default behaves similarly to derive, except that it only executes once.

new verymodel.Model({
    comment: {
        type: 'string',
        default: function () {
            return this.author.fullName + ' has nothing to say.';
        },
    },
    author: {foreignKey: 'user'},
    starredBy: {foreignCollection: 'user'}
});

❗ Warning! Assigning mutable objects as a default can result in the default getting changed over time. When assigning objects, arrays, or essentially any advanced type, set default to a function that returns a new instance of the object.


private

private is a boolean, false by default, which determines whether a field is included in the object resulting from toJSON().

Model Factory Methods

create(value_object, options)

Returns a factory instance model.

Create makes a new instance of the model with specific data. Any fields in the value_object that were not defined get thrown out. Validations are not done on creation, but some values may be processed based on the field definition type and processIn functions.

Logging the model out to console will produce a confusing result. If you want the model's data, run .toJSON() and use the result.

Options:

  • processors: Array of strings listing which custom processors to run in each fields definition for processor

Example:

//assuming Person is a defined Model Factory
var person = Person.create({
    firstName: 'Nathan',
    lastName: 'Fritz',
});

exportJoi(fields)

Returns a Joi.object() of all of the field definitions that include joi validators. Useful for using in hapi validators and validating without a model instance.

Arguments:

  • fields - An optional array of fields to include, or an object with a root property of fields, and other properties of submodel fields

Model Instance Methods


toJSON(flags)

Outputs a JSON style object from the model.

Options:

  • noDepth: false by default. If true, does not recursively toJSON objects like models and collections.
  • withPrivate: false by default. If true, includes fields with private set to true.
  • processors: list of processors to run.

Example:

You want an example? Look at all of the other examples... most of them use toJSON.

☝️ toJSON does not produce a string, but an object. See: toString.


toString()

Just like toJSON, but produces a JSON string rather than an object.


diff(other)

Arguments:

  • other: model instance to compare this one to.

Result: object of each field with left, and right values.

{
    firstName: {left: 'Nathan', right: 'Sam'},
    lastName: {left: 'Fritz', right: 'Fritz'},
}

getChanges()

Get the changes since create.

Result: object of each field with then, now, and changed boolean.

{
    body: {then: "I dont liek cheese.", now: "I don't like cheese.", changed: true},
    updated: {then: '2014-02-10 11:11:11', now: '2014-02-10 12:12:12', changed: true},
    created: {then: '2014-02-10 11:11:11', now: '2014-02-10 11:11:11', changed: false},
}

getOldModel()

Get a new model instance of this instance with all of the changes since create reversed.

Result: Model instance.


loadData()

Loads data just like when a model instance is retrieved or created.

processIn is called on any fields specified, but onSet is not.

Essentially the same things happen as when running create but can be done after the model instance is initialized.

Example:

var person = Person.create({
    firstName: 'Nathan',
    lastName: 'Fritz',
});

person.favoriteColor = 'blue';

person.loadData({
    favoriteColor: 'green',
    favoriteFood: 'burrito',
});

console.log(person.toJSON());
// {firstName: 'Nathan', lastName: 'Fritz', favoriteFood: 'burrito', favoriteColor: 'green'}
``