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.
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.
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.
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.
- Adding Functionality
- Extended Example
- Field Definitions
- Model Options
- Model Factory Methods
- Model Instance Methods
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);
}
});
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
- model
- collection
- validate
- processIn
- processOut
- processors
- onSet
- derive
- index
- default
- derive
- depends
- private
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}}
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.
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.
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
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
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
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
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
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
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
is a boolean, false by default, which determines whether a field is included in the object resulting from toJSON().
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',
});
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
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.
Just like toJSON, but produces a JSON string rather than an object.
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'},
}
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},
}
Get a new model instance of this instance with all of the changes since create reversed.
Result: Model instance.
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'}
``