Plaster
Simple Mongoose-inspired schema based Javascript object modelling
Node.js >= 0.12 supported. For all features, run node with the harmony --harmony
and harmony proxies --harmony_proxies
flags.
Installation
npm install plaster
Overview
Plaster is a simple, Mongoose-inspired schema based Javascript object modelling library. Just define your schemas and
create Javascript classes from them.
var plaster = require('plaster');
var schema = plaster.schema({ name: String });
var Cat = plaster.model('Cat', schema);
var kitty = new Cat({ name: 'Zildjian' });
console.log(kitty);
Features:
- Schema definition
- Strict modelling based on schema
- Schema extension
- Automatic type validation and custom validation
- Middleware including pre and post hooks
Guide
Modelling
Basics
We begin defining a data model using a schema.
var userSchema = plaster.schema({
firstName: String,
lastName: String,
age: Number,
usernames: [String],
setup: Boolean
metadata: {
createdAt: Date,
updatedAt: Date
}
});
We can add additional properties using add
function:
userSchema.add('name', String);
Alternatively we can explicitly specify the type using type
property:
var catSchema = plaster.schema({
name: { type: String }
breed: String,
});
catSchema.add('age', {type: String});
Schema options can be set at construction or using the set
function.
var catSchema = plaster.schema({
name: { type: String }
breed: String,
});
catSchema.set('minimize', false);
Validation
Plaster does automatic validation against input data using the type information specified in the schema definition.
We can provide custom validation in schema definition by providing validator
function.
var validator = require('validator'); // Node validator module
var userSchema = plaster.schema({
name: String
email: {type: String, validate: validator.isEmail}
});
var User = plaster.model('User', userSchema);
var user = new User({ name: 'Bob Smith' });
user.email = 'bob@gmail.com'; // OK
user.email = 'bsmith'; // Nope
console.log(user.email); // 'bob@gmail.com'
Virtuals
Virtuals are document properties that you can get and set but that do not get persisted to the database.
The getters are useful for formatting or combining fields, while setters are useful for de-composing a single value
into multiple values for storage.
var userSchema = plaster.schema({
firstName: String,
lastName: String
});
userSchema.virtual('fullName', {
get: function () {
return this.firstName + ' ' + this.lastName;
},
set: function (v) {
if (v !== undefined) {
var parts = v.split(' ');
this.firstName = parts[0];
this.lastName = parts[1];
}
}
});
var User = plaster.model('User', userSchema);
var user = new User({firstName: 'Bob', lastName: 'Smith'});
console.log(user.fullName); // Bob Smith
user.fullName = 'Jim Jones';
console.log(user.fullName); // Jim Jones
console.log(user.firstName); // Jim
console.log(user.lastName); // Jones
If no set
function is defined the virtual is read-only.
Statics
Adding static methods to Models can be accomplished using static()
schema function
var userSchema = plaster.schema({
firstName: String,
lastName: String
});
userSchema.static('foo', function(p, q) {
return p + q;
});
var User = plaster.model('User', userSchema);
User.foo(1, 2); // 3
We can also pass an object of function keys and function values, and they will all be added.
Methods
Similarly adding instance methods to Models can be done using method()
schema function.
var userSchema = plaster.schema({
firstName: String,
lastName: String
});
userSchema.method('fullName', function() {
return this.firstName + ' ' + this.lastName;
});
var User = plaster.model('User', userSchema);
var user = new User({firstName: 'Bob', lastName: 'Smith'});
user.fullName(); // 'Bob Smith'
We can also pass an object of function keys and function values, and they will all be added.
init() method
There is a special init
method that if specified in schema definition will be called at the end of model creation.
You can do additional setup here. This method is not passed in any arguments.
toObject()
Model instances come with toObject
function that is automatically used for console.log
inspection.
Options:
transform
- function used to transform an object once it's been converted to plain javascript representation from a
model instance.minimize
- to "minimize" the document by removing any empty properties. Default:true
virtuals
- to apply virtual getters
These settings can be applied on any invocation of toObject
as well they can be set at schema level.
var userSchema = plaster.schema({
name: String,
email: String,
password: String
});
var xform = function (doc, ret, options) {
delete ret.password;
return ret;
};
userSchema.set('toObject', {transform: xform});
var User = plaster.model('User', userSchema);
var user = new User({
name: 'Joe',
email: 'joe@gmail.com',
password: 'password'
});
console.log(user); // { name: 'Joe', email: 'joe@gmail.com' }
toJSON()
Similar to toObject
. The return value of this method is used in calls to JSON.stringify
.
Middleware
Similar to Mongoose middleware, we exposes pre
and post
hooks.
onBeforeValueSet(key, value) / onValueSet(key, value)
onBeforeValueSet
/ onValueSet
allow you to bind an event handler to all write operations on an object.
Currently, it will only notify of write operations on the object itself and will not notify you when child objects are
written to. If you return false or throw an error within the onBeforeValueSet handler, the write operation will be
cancelled. Throwing an error will add the error to the error stack.
var User = plaster.schema({ name: String }, {
onBeforeValueSet: function(key, value) {
if(key === 'name' && value.indexOf('Joe') >= 0) {
return false;
});
}
});
var User = plaster.model('User', schema);
var user = new User();
user.name = 'Bill'; // { name: undefined }
user.name = 'Joe Smith'; // { name: 'Joe Smith' }
Types
Supported types:
- String
- Number
- Boolean
- Date
- Array (including types within Array)
- Object (including typed Models for sub-schemas)
- 'any'
When a type is specified, it will be enforced. Typecasting is enforced on all types except 'any'. If a value cannot be typecasted to the correct type, the original value will remain untouched.
Types can be extended with a variety of attributes. Some attributes are type-specific and some apply to all types.
Custom types can be created by defining an object with type properties.
var NotEmptyString = {type: String, minLength: 1};
country: {type: NotEmptyString, default: 'USA'}
General attributes
transform
Called immediately when value is set and before any typecast is done.
name: {type: String, transform: function(value) {
// Modify the value here...
return value;
}}
validate
Called immediately when value is set and before any typecast is done. Can be used for validating input data.
If you return false
the write operation will be cancelled.
name: {type: String, validate: function(value) {
// check
return value;
}}
default
Provide default value. You may pass value directly or pass a function which will be executed when the object is initialized. The function is executed in the context of the object and can use "this" to access other properties (which .
country: {type: String, default: 'USA'}
get
Provide function to transform value when retrieved. Executed in the context of the object and can use "this" to access properties.
string: {type: String, getter: function(value) { return value.toUpperCase(); }}
readOnly
If true, the value can be read but cannot be written to. This can be useful for creating fields that reflect other values.
fullName: {type: String, readOnly: true, default: function(value) {
return (this.firstName + ' ' + this.lastName).trim();
}}
invisible
If true, the value can be written to but isn't outputted as an index when toObject()
is called.
This can be useful for hiding internal variables.
String
stringTransform
Called after value is typecast to string if value was successfully typecast but called before all validation.
postalCode: {type: String, stringTransform: function(string) {
// Type will ALWAYS be String, so using string prototype is OK.
return string.toUpperCase();
}}
regex
Validates string against Regular Expression. If string doesn't match, it's rejected.
memberCode: {type: String, regex: new RegExp('^([0-9A-Z]{4})$')}
enum
Validates string against array of strings. If not present, it's rejected.
gender: {type: String, enum: ['m', 'f']}
minLength
Enforces minimum string length.
notEmpty: {type: String, minLength: 1}
maxLength
Enforces maximum string length.
stateAbbrev: {type: String, maxLength: 2}
clip
If true, clips string to maximum string length instead of rejecting string.
bio: {type: String, maxLength: 255, clip: true}
Number
min
Number must be > min attribute or it's rejected.
positive: {type: Number, min: 0}
max
Number must be < max attribute or it's rejected.
negative: {type: Number, max: 0}
Array
unique
Ensures duplicate-free array, using === to test object equality.
emails: {type: Array, unique: true, arrayType: String}
arrayType
Elements within the array will be typed to the attributes defined.
aliases: {type: Array, arrayType: {type: String, minLength: 1}}
An alternative shorthand version is also available -- wrap the properties within array brackets.
aliases: [{type: String, minLength: 1}]
Object
objectType
Allows you to define a typed object.
company: {type: Object, objectType: {
name: String
}}
An alternative shorthand version is also available -- simply pass a descriptor.
company: {
name: String
}
Alias
index (required)
The index key of the property being aliased.
zip: String,
postalCode: {type: 'alias', index: 'zip'}
// this.postalCode = 12345 -> this.toObject() -> {zip: '12345'}
Schema Extension
It is useful to have a common base schema, that all other schemas / models would extend or "inherit" properties from.
This can be accomplished by using the Schema.extend
function. When used all properties, virtuals,
methods, statics, and middleware that are present in the base schema but not present in destination schema are copied
into the destination schema.
var baseSchema = plaster.schema({
metadata: {
doc_type: String,
createdAt: Date,
updatedAt: Date
}
});
baseSchema.method('save', function(fn) {
// simulate some async operation
var self = this;
process.nextTick(function() {
return fn(null, self);
});
});
baseSchema.pre('save', function (next) {
if (!this.metadata) {
this.metadata = {};
}
var now = new Date();
if (!this.metadata.createdAt) {
this.metadata.createdAt = now;
}
this.metadata.updatedAt = now;
this.metadata.doc_type = this.modelName;
next();
});
baseSchema.method('baseFoo', function () {
console.log('base foo');
});
var userSchema = plaster.schema({
name: String,
email: String,
});
userSchema.method('save', function(fn) {
// simulate some other async operation
var self = this;
process.nextTick(function() {
return fn(null, self);
});
});
userSchema.pre('save', function (next) {
if (this.email) {
this.email = this.email.toLowerCase();
}
next();
});
userSchema.method('userFoo', function () {
console.log('user foo');
});
// make user schema extend the base schema
userSchema.extend(baseSchema);
var User = plaster.model('User', userSchema);
user = new User({
name: 'Bob Smith',
email: 'BSmith@gmail.com'
});
user.baseFoo() // prints 'base foo'
user.userFoo() // prints 'user foo'
user.save(function(err, savedDoc) {
console.log(user.metadata.updatedAt); // Sat Dec 29 2015 03:30:00 GMT-0400 (AST)
console.log(user.metadata.doc_type); // 'user'
console.log(user.email); // 'bsmith@gmail.com'
});
Errors
When setting a value fails, an error is generated silently. Errors can be retrieved with getErrors()
and cleared with clearErrors()
.
var schema = new plaster.schema({
id: {type: String, minLength: 5}
});
var Profile = plaster.model('Profile', schema);
var profile = new Profile();
profile.id = '1234';
console.log(profile.hasErrors()); // true
console.log(profile.getErrors());
// Prints:
[ { errorMessage: 'String length too short to meet minLength requirement.',
setValue: '1234',
originalValue: undefined,
fieldSchema: { name: 'id', type: 'string', minLength: 5 } } ]
// Clear all errors.
profile.clearErrors();
Tests
Module automated tests can be run using npm test
command.
Credits
Lots of code and design inspired by Mongoose.
Uses modified code from node-schema-object for modelling.
License
Copyright 2016 Bojan D.
Licensed under the MIT License.