Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Document

  • Loading branch information...
commit 6ee7de0716d0b6f5f5a537ea549cb55cfbc6fc57 1 parent 30d0818
@1602 authored
View
9 index.js
@@ -1,11 +1,18 @@
var fs = require('fs');
+var path = require('path');
exports.Schema = require('./lib/schema').Schema;
exports.AbstractClass = require('./lib/abstract-class').AbstractClass;
exports.Validatable = require('./lib/validatable').Validatable;
+exports.init = function () {
+ if (!global.railway) return;
+ railway.orm = exports;
+ require('./lib/railway');
+};
+
try {
- if (process.versions.node < '0.6' || true) {
+ if (process.versions.node < '0.6') {
exports.version = JSON.parse(fs.readFileSync(__dirname + '/package.json')).version;
} else {
exports.version = require('../package').version;
View
211 lib/abstract-class.js
@@ -1,10 +1,10 @@
/**
- * Module deps
+ * Module dependencies
*/
-var Validatable = require('./validatable').Validatable;
-var Hookable = require('./hookable').Hookable;
var util = require('util');
var jutil = require('./jutil');
+var Validatable = require('./validatable').Validatable;
+var Hookable = require('./hookable').Hookable;
var DEFAULT_CACHE_LIMIT = 1000;
exports.AbstractClass = AbstractClass;
@@ -13,7 +13,15 @@ jutil.inherits(AbstractClass, Validatable);
jutil.inherits(AbstractClass, Hookable);
/**
- * Abstract class constructor
+ * Abstract class - base class for all persist objects
+ * provides **common API** to access any database adapter.
+ * This class describes only abstract behavior layer, refer to `lib/adapters/*.js`
+ * to learn more about specific adapter implementations
+ *
+ * `AbstractClass` mixes `Validatable` and `Hookable` classes methods
+ *
+ * @constructor
+ * @param {Object} data - initial object data
*/
function AbstractClass(data) {
var self = this;
@@ -99,6 +107,10 @@ function AbstractClass(data) {
AbstractClass.setter = {};
AbstractClass.getter = {};
+/**
+ * @param {String} prop - property name
+ * @param {Object} params - various property configuration
+ */
AbstractClass.defineProperty = function (prop, params) {
this.schema.defineProperty(this.modelName, prop, params);
};
@@ -113,8 +125,14 @@ AbstractClass.prototype.whatTypeName = function (propName) {
};
/**
+ * Create new instance of Model class, saved in database
+ *
* @param data [optional]
* @param callback(err, obj)
+ * callback called with arguments:
+ *
+ * - err (null or Error)
+ * - instance (null or Model)
*/
AbstractClass.create = function (data, callback) {
if (stillConnecting(this.schema, this, arguments)) return;
@@ -178,6 +196,9 @@ function stillConnecting(schema, obj, args) {
return true;
};
+/**
+ * Update or insert
+ */
AbstractClass.upsert = AbstractClass.updateOrCreate = function upsert(data, callback) {
if (stillConnecting(this.schema, this, arguments)) return;
@@ -204,6 +225,12 @@ AbstractClass.upsert = AbstractClass.updateOrCreate = function upsert(data, call
}
};
+/**
+ * Check whether object exitst in database
+ *
+ * @param {id} id - identifier of object (primary key value)
+ * @param {Function} cb - callbacl called with (err, exists: Bool)
+ */
AbstractClass.exists = function exists(id, cb) {
if (stillConnecting(this.schema, this, arguments)) return;
@@ -214,6 +241,12 @@ AbstractClass.exists = function exists(id, cb) {
}
};
+/**
+ * Find object by id
+ *
+ * @param {id} id - primary key value
+ * @param {Function} cb - callback called with (err, instance)
+ */
AbstractClass.find = function find(id, cb) {
if (stillConnecting(this.schema, this, arguments)) return;
@@ -236,9 +269,20 @@ AbstractClass.find = function find(id, cb) {
};
/**
- * Query collection of objects
- * @param params {where: {}, order: '', limit: 1, offset: 0,...}
- * @param cb (err, array of AbstractClass)
+ * Find all instances of Model, matched by query
+ * make sure you have marked as `index: true` fields for filter or sort
+ *
+ * @param {Object} params (optional)
+ *
+ * - where: Object `{ key: val, key2: {gt: 'val2'}}`
+ * - order: String
+ * - limit: Number
+ * - skip: Number
+ *
+ * @param {Function} callback (required) called with arguments:
+ *
+ * - err (null or Error)
+ * - Array of instances
*/
AbstractClass.all = function all(params, cb) {
if (stillConnecting(this.schema, this, arguments)) return;
@@ -271,6 +315,12 @@ AbstractClass.all = function all(params, cb) {
});
};
+/**
+ * Find one record, same as `all`, limited by 1 and return object, not collection
+ *
+ * @param {Object} params - search conditions
+ * @param {Function} cb - callback called with (err, instance)
+ */
AbstractClass.findOne = function findOne(params, cb) {
if (stillConnecting(this.schema, this, arguments)) return;
@@ -293,6 +343,10 @@ function substractDirtyAttributes(object, data) {
});
}
+/**
+ * Destroy all records
+ * @param {Function} cb - callback called with (err)
+ */
AbstractClass.destroyAll = function destroyAll(cb) {
if (stillConnecting(this.schema, this, arguments)) return;
@@ -302,6 +356,12 @@ AbstractClass.destroyAll = function destroyAll(cb) {
}.bind(this));
};
+/**
+ * Return count of matched records
+ *
+ * @param {Object} where - search conditions (optional)
+ * @param {Function} cb - callback, called with (err, count)
+ */
AbstractClass.count = function (where, cb) {
if (stillConnecting(this.schema, this, arguments)) return;
@@ -312,6 +372,11 @@ AbstractClass.count = function (where, cb) {
this.schema.adapter.count(this.modelName, cb, where);
};
+/**
+ * Return string representation of class
+ *
+ * @override default toString method
+ */
AbstractClass.toString = function () {
return '[Model ' + this.modelName + ']';
}
@@ -393,14 +458,22 @@ AbstractClass.prototype.isNewRecord = function () {
return !this.id;
};
+/**
+ * Return adapter of current record
+ * @private
+ */
AbstractClass.prototype._adapter = function () {
return this.constructor.schema.adapter;
};
-AbstractClass.prototype.propertyChanged = function (name) {
- return this[name + '_was'] !== this['_' + name];
-};
-
+/**
+ * Convert instance to Object
+ *
+ * @param {Boolean} onlySchema - restrict properties to schema only, default false
+ * when onlySchema == true, only properties defined in schema returned,
+ * otherwise all enumerable properties returned
+ * @returns {Object} - canonical object representation (no getters and setters)
+ */
AbstractClass.prototype.toObject = function (onlySchema) {
var data = {};
var ds = this.constructor.schema.definitions[this.constructor.modelName];
@@ -412,6 +485,11 @@ AbstractClass.prototype.toObject = function (onlySchema) {
return data;
};
+/**
+ * Delete object from persistence
+ *
+ * @triggers `destroy` hook (async) before and after destroying object
+ */
AbstractClass.prototype.destroy = function (cb) {
if (stillConnecting(this.constructor.schema, this, arguments)) return;
@@ -425,14 +503,30 @@ AbstractClass.prototype.destroy = function (cb) {
});
};
-AbstractClass.prototype.updateAttribute = function (name, value, cb) {
- if (stillConnecting(this.constructor.schema, this, arguments)) return;
-
+/**
+ * Update single attribute
+ *
+ * equals to `updateAttributes({name: value}, cb)
+ *
+ * @param {String} name - name of property
+ * @param {Mixed} value - value of property
+ * @param {Function} callback - callback called with (err, instance)
+ */
+AbstractClass.prototype.updateAttribute = function updateAttribute(name, value, callback) {
data = {};
data[name] = value;
- this.updateAttributes(data, cb);
+ this.updateAttributes(data, callback);
};
+/**
+ * Update set of attributes
+ *
+ * this method performs validation before updating
+ *
+ * @trigger `validation`, `save` and `update` hooks
+ * @param {Object} data - data to update
+ * @param {Function} callback - callback called with (err, instance)
+ */
AbstractClass.prototype.updateAttributes = function updateAttributes(data, cb) {
if (stillConnecting(this.constructor.schema, this, arguments)) return;
@@ -490,23 +584,34 @@ AbstractClass.prototype.updateAttributes = function updateAttributes(data, cb) {
/**
* Checks is property changed based on current property and initial value
- * @param {attr} String - property name
+ *
+ * @param {String} attr - property name
* @return Boolean
*/
-AbstractClass.prototype.propertyChanged = function (attr) {
+AbstractClass.prototype.propertyChanged = function propertyChanged(attr) {
return this['_' + attr] !== this[attr + '_was'];
};
-AbstractClass.prototype.reload = function (cb) {
+/**
+ * Reload object from persistence
+ *
+ * @requires `id` member of `object` to be able to call `find`
+ * @param {Function} callback - called with (err, instance) arguments
+ */
+AbstractClass.prototype.reload = function reload(callback) {
if (stillConnecting(this.constructor.schema, this, arguments)) return;
var obj = getCached(this.constructor, this.id);
- if (obj) {
- obj.reset();
- }
- this.constructor.find(this.id, cb);
+ if (obj) obj.reset();
+ this.constructor.find(this.id, callback);
};
+/**
+ * Reset dirty attributes
+ *
+ * this method does not perform any database operation it just reset object to it's
+ * initial state
+ */
AbstractClass.prototype.reset = function () {
var obj = this;
Object.keys(obj).forEach(function (k) {
@@ -519,8 +624,14 @@ AbstractClass.prototype.reset = function () {
});
};
-// relations
-AbstractClass.hasMany = function (anotherClass, params) {
+/**
+ * Declare hasMany relation
+ *
+ * @param {Class} anotherClass - class to has many
+ * @param {Object} params - configuration {as:, foreignKey:}
+ * @example `User.hasMany(Post, {as: 'posts', foreignKey: 'authorId'});`
+ */
+AbstractClass.hasMany = function hasMany(anotherClass, params) {
var methodName = params.as; // or pluralize(anotherClass.modelName)
var fk = params.foreignKey;
// each instance of this class should have method named
@@ -562,6 +673,12 @@ AbstractClass.hasMany = function (anotherClass, params) {
};
+/**
+ * Declare belongsTo relation
+ *
+ * @param {Class} anotherClass - class to belong
+ * @param {Object} params - configuration {as: 'propertyName', foreignKey: 'keyName'}
+ */
AbstractClass.belongsTo = function (anotherClass, params) {
var methodName = params.as;
var fk = params.foreignKey;
@@ -596,6 +713,10 @@ AbstractClass.belongsTo = function (anotherClass, params) {
};
+/**
+ * Define scope
+ * TODO: describe behavior and usage examples
+ */
AbstractClass.scope = function (name, params) {
defineScope(this, this, name, params);
};
@@ -689,14 +810,23 @@ function defineScope(cls, targetClass, name, params, methods) {
}
}
-// helper methods
-//
+/**
+ * Check whether `s` is not undefined
+ * @param {Mixed} s
+ * @return {Boolean} s is undefined
+ */
function isdef(s) {
var undef;
return s !== undef;
}
+/**
+ * Merge `base` and `update` params
+ * @param {Object} base - base object (updating this object)
+ * @param {Object} update - object with new data to update base
+ * @returns {Object} `base`
+ */
function merge(base, update) {
base = base || {};
if (update) {
@@ -707,6 +837,13 @@ function merge(base, update) {
return base;
}
+/**
+ * Define readonly property on object
+ *
+ * @param {Object} obj
+ * @param {String} key
+ * @param {Mixed} value
+ */
function defineReadonlyProp(obj, key, value) {
Object.defineProperty(obj, key, {
writable: false,
@@ -716,11 +853,17 @@ function defineReadonlyProp(obj, key, value) {
});
}
+/**
+ * Add object to cache
+ */
function addToCache(constr, obj) {
touchCache(constr, obj.id);
constr.cache[obj.id] = obj;
}
+/**
+ * Renew object position in LRU cache index
+ */
function touchCache(constr, id) {
var cacheLimit = constr.CACHE_LIMIT || DEFAULT_CACHE_LIMIT;
@@ -734,16 +877,32 @@ function touchCache(constr, id) {
}
}
+/**
+ * Retrieve cached object
+ */
function getCached(constr, id) {
if (id) touchCache(constr, id);
return id && constr.cache[id];
}
+/**
+ * Clear cache (fully)
+ *
+ * removes both cache and LRU index
+ *
+ * @param {Class} constr - class constructor
+ */
function clearCache(constr) {
constr.cache = {};
constr.mru = [];
}
+/**
+ * Remove object from cache
+ *
+ * @param {Class} constr
+ * @param {id} id
+ */
function removeFromCache(constr, id) {
var ind = constr.mru.indexOf(id);
if (!~ind) constr.mru.splice(ind, 1);
View
3  lib/adapters/mysql.js
@@ -35,6 +35,9 @@ exports.initialize = function initializeSchema(schema, callback) {
});
};
+/**
+ * MySQL adapter
+ */
function MySQL(client) {
this._models = {};
this.client = client;
View
2  lib/adapters/redis.js
@@ -54,7 +54,7 @@ BridgeToRedis.prototype.save = function (model, data, callback) {
BridgeToRedis.prototype.updateIndexes = function (model, id, data, callback) {
var i = this.indexes[model];
- var schedule = [];
+ var schedule = [['sadd', 's:' + model, id]];
Object.keys(data).forEach(function (key) {
if (i[key]) {
schedule.push([
View
199 lib/railway.js
@@ -0,0 +1,199 @@
+var fs = require('fs');
+var path = require('path');
+var Schema = railway.orm.Schema;
+
+railway.orm._schemas = [];
+
+try {
+ var config = JSON.parse(fs.readFileSync(app.root + '/config/database.json', 'utf-8'))[app.set('env')];
+} catch (e) {
+ console.log('Could not parse config/database.json');
+ throw e;
+}
+
+var schema = new Schema(config && config.driver || 'memory', config);
+schema.log = log;
+railway.orm._schemas.push(schema);
+
+context = prepareContext(schema);
+
+// run schema first
+var schemaFile = app.root + '/db/schema.';
+if (path.existsSync(schemaFile + 'js')) {
+ schemaFile += 'js';
+} else {
+ schemaFile += 'coffee';
+}
+runCode(schemaFile, context);
+
+// and freeze schemas
+railway.orm._schemas.forEach(function (schema) {
+ schema.freeze();
+});
+
+function log(str, startTime) {
+ var $ = utils.stylize.$;
+ var m = Date.now() - startTime;
+ utils.debug(str + $(' [' + (m < 10 ? m : $(m).red) + ' ms]').bold);
+ app.emit('app-event', {
+ type: 'query',
+ param: str,
+ time: m
+ });
+}
+
+function runCode(filename, context) {
+ var isCoffee = filename.match(/coffee$/);
+
+ context = context || {};
+
+ var dirname = path.dirname(filename);
+
+ // extend context
+ context.require = context.require || function (apath) {
+ var isRelative = apath.match(/^\.\.?\//);
+ return require(isRelative ? path.resolve(dirname, apath) : apath);
+ };
+ context.app = app;
+ context.railway = railway;
+ context.console = console;
+ context.setTimeout = setTimeout;
+ context.setInterval = setInterval;
+ context.clearTimeout = clearTimeout;
+ context.clearInterval = clearInterval;
+ context.__filename = filename;
+ context.__dirname = dirname;
+ context.process = process;
+ context.t = context.t || t;
+ context.Buffer = Buffer;
+
+ var code = path.existsSync(filename) && require('fs').readFileSync(filename);
+ if (!code) return;
+ if (isCoffee) {
+ try {
+ var cs = require('coffee-script');
+ } catch (e) {
+ throw new Error('Please install coffee-script npm package: `npm install coffee-script`');
+ }
+ try {
+ code = require('coffee-script').compile(code);
+ } catch (e) {
+ console.log('Error in coffee code compilation in file ' + filename);
+ throw e;
+ }
+ }
+
+ try {
+ var m = require('vm').createScript(code.toString('utf8'), filename);
+ m.runInNewContext(context);
+ } catch (e) {
+ console.log('Error while executing ' + filename);
+ throw e;
+ }
+
+}
+
+function prepareContext(defSchema, done) {
+ var ctx = {app: app},
+ models = {},
+ settings = {},
+ cname,
+ schema,
+ wait = connected = 0,
+ nonJugglingSchema = false;
+
+ done = done || function () {};
+
+ /**
+ * Multiple schemas support
+ * example:
+ * schema('redis', {url:'...'}, function () {
+ * describe models using redis connection
+ * ...
+ * });
+ * schema(function () {
+ * describe models stored in memory
+ * ...
+ * });
+ */
+ ctx.schema = function () {
+ var name = argument('string');
+ var opts = argument('object') || {};
+ var def = argument('function') || function () {};
+ schema = new Schema(name || opts.driver || 'memory', opts);
+ railway.orm._schemas.push(schema);
+ wait += 1;
+ ctx.gotSchema = true;
+ schema.on('log', log);
+ schema.on('connected', function () {
+ if (wait === ++connected) done();
+ });
+ def();
+ schema = false;
+ };
+
+ /**
+ * Use custom schema driver
+ */
+ ctx.customSchema = function () {
+ var def = argument('function') || function () {};
+ nonJugglingSchema = true;
+ def();
+ Object.keys(ctx.exports).forEach(function (m) {
+ ctx.define(m, ctx.exports[m]);
+ });
+ nonJugglingSchema = false;
+ };
+ ctx.exports = {};
+ ctx.module = { exports: ctx.exports };
+
+ /**
+ * Define a class in current schema
+ */
+ ctx.describe = ctx.define = function (className, callback) {
+ var m;
+ cname = className;
+ models[cname] = {};
+ settings[cname] = {};
+ if (nonJugglingSchema) {
+ m = callback;
+ } else {
+ callback && callback();
+ m = (schema || defSchema).define(className, models[cname], settings[cname]);
+ }
+ return global[cname] = app.models[cname] = ctx[cname] = m;
+ };
+
+ /**
+ * Define a property in current class
+ */
+ ctx.property = function (name, type, params) {
+ if (!params) params = {};
+ if (typeof type !== 'function' && typeof type === 'object') {
+ params = type;
+ type = String;
+ }
+ params.type = type || String;
+ models[cname][name] = params;
+ };
+
+ /**
+ * Set custom table name for current class
+ * @param name - name of table
+ */
+ ctx.setTableName = function (name) {
+ if (cname) settings[cname].table = name;
+ };
+
+ ctx.Text = Schema.Text;
+
+ return ctx;
+
+ function argument(type) {
+ var r;
+ [].forEach.call(arguments.callee.caller.arguments, function (a) {
+ if (!r && typeof a === type) r = a;
+ });
+ return r;
+ }
+}
View
198 lib/schema.js
@@ -17,10 +17,30 @@ exports.Schema = Schema;
var slice = Array.prototype.slice;
/**
- * Shema - classes factory
+ * Schema - adapter-specific classes factory.
+ *
+ * All classes in single schema shares same adapter type and
+ * one database connection
+ *
* @param name - type of schema adapter (mysql, mongoose, sequelize, redis)
* @param settings - any database-specific settings which we need to
* establish connection (of course it depends on specific adapter)
+ *
+ * - host
+ * - port
+ * - username
+ * - password
+ * - database
+ * - debug {Boolean} = false
+ *
+ * @example Schema creation, waiting for connection callback
+ * ```
+ * var schema = new Schema('mysql', { database: 'myapp_test' });
+ * schema.define(...);
+ * schema.on('connected', function () {
+ * // work with database
+ * });
+ * ```
*/
function Schema(name, settings) {
var schema = this;
@@ -73,65 +93,40 @@ function Schema(name, settings) {
util.inherits(Schema, require('events').EventEmitter);
-function Text() {
-}
+function Text() {}
Schema.Text = Text;
-Schema.prototype.defineProperty = function (model, prop, params) {
- this.definitions[model].properties[prop] = params;
- if (this.adapter.defineProperty) {
- this.adapter.defineProperty(model, prop, params);
- }
-};
-
-Schema.prototype.automigrate = function (cb) {
- this.freeze();
- if (this.adapter.automigrate) {
- this.adapter.automigrate(cb);
- } else if (cb) {
- cb();
- }
-};
-
-Schema.prototype.autoupdate = function (cb) {
- this.freeze();
- if (this.adapter.autoupdate) {
- this.adapter.autoupdate(cb);
- } else if (cb) {
- cb();
- }
-};
-
-/**
- * Check whether migrations needed
- */
-Schema.prototype.isActual = function (cb) {
- this.freeze();
- if (this.adapter.isActual) {
- this.adapter.isActual(cb);
- } else if (cb) {
- cb(null, true);
- }
-};
-
-Schema.prototype.log = function (sql, t) {
- this.emit('log', sql, t);
-};
-
-Schema.prototype.freeze = function freeze() {
- if (this.adapter.freezeSchema) {
- this.adapter.freezeSchema();
- }
-}
-
/**
* Define class
- * @param className
- * @param properties - hash of class properties in format
- * {property: Type, property2: Type2, ...}
- * or
- * {property: {type: Type}, property2: {type: Type2}, ...}
- * @param settings - other configuration of class
+ *
+ * @param {String} className
+ * @param {Object} properties - hash of class properties in format
+ * `{property: Type, property2: Type2, ...}`
+ * or
+ * `{property: {type: Type}, property2: {type: Type2}, ...}`
+ * @param {Object} settings - other configuration of class
+ * @return newly created class
+ *
+ * @example simple case
+ * ```
+ * var User = schema.defind('User', {
+ * email: String,
+ * password: String,
+ * birthDate: Date,
+ * activated: Boolean
+ * });
+ * ```
+ *
+ * @example more advanced case
+ * ```
+ * var User = schema.defind('User', {
+ * email: { type: String, limit: 150, index: true },
+ * password: { type: String, limit: 50 },
+ * birthDate: Date,
+ * registrationDate: {type: Date, default: function () { return new Date }},
+ * activated: { type: Boolean, default: false }
+ * });
+ * ```
*/
Schema.prototype.define = function defineClass(className, properties, settings) {
var schema = this;
@@ -191,10 +186,94 @@ Schema.prototype.define = function defineClass(className, properties, settings)
};
+
+/**
+ * Define single property named `prop` on `model`
+ *
+ * @param {String} model - name of model
+ * @param {String} prop - name of propery
+ * @param {Object} params - property settings
+ */
+Schema.prototype.defineProperty = function (model, prop, params) {
+ this.definitions[model].properties[prop] = params;
+ if (this.adapter.defineProperty) {
+ this.adapter.defineProperty(model, prop, params);
+ }
+};
+
+/**
+ * Drop each model table and re-create.
+ * This method make sense only for sql adapters.
+ *
+ * @warning All data will be lost! Use autoupdate if you need your data.
+ */
+Schema.prototype.automigrate = function (cb) {
+ this.freeze();
+ if (this.adapter.automigrate) {
+ this.adapter.automigrate(cb);
+ } else if (cb) {
+ cb();
+ }
+};
+
+/**
+ * Update existing database tables.
+ * This method make sense only for sql adapters.
+ */
+Schema.prototype.autoupdate = function (cb) {
+ this.freeze();
+ if (this.adapter.autoupdate) {
+ this.adapter.autoupdate(cb);
+ } else if (cb) {
+ cb();
+ }
+};
+
+/**
+ * Check whether migrations needed
+ * This method make sense only for sql adapters.
+ */
+Schema.prototype.isActual = function (cb) {
+ this.freeze();
+ if (this.adapter.isActual) {
+ this.adapter.isActual(cb);
+ } else if (cb) {
+ cb(null, true);
+ }
+};
+
+/**
+ * Log benchmarked message. Do not redefine this method, if you need to grab
+ * chema logs, use `schema.on('log', ...)` emitter event
+ *
+ * @private used by adapters
+ */
+Schema.prototype.log = function (sql, t) {
+ this.emit('log', sql, t);
+};
+
+/**
+ * Freeze schema. Behavior depends on adapter
+ */
+Schema.prototype.freeze = function freeze() {
+ if (this.adapter.freezeSchema) {
+ this.adapter.freezeSchema();
+ }
+}
+
+/**
+ * Return table name for specified `modelName`
+ * @param {String} modelName
+ */
Schema.prototype.tableName = function (modelName) {
return this.definitions[modelName].settings.table = this.definitions[modelName].settings.table || modelName
};
+/**
+ * Define foreign key
+ * @param {String} className
+ * @param {String} key - name of key field
+ */
Schema.prototype.defineForeignKey = function defineForeignKey(className, key) {
// return if already defined
if (this.definitions[className].properties[key]) return;
@@ -209,13 +288,18 @@ Schema.prototype.defineForeignKey = function defineForeignKey(className, key) {
}
};
+/**
+ * Close database connection
+ */
Schema.prototype.disconnect = function disconnect() {
if (typeof this.adapter.disconnect === 'function') {
this.adapter.disconnect();
}
};
-
+/**
+ * Define hidden property
+ */
function hiddenProperty(where, property, value) {
Object.defineProperty(where, property, {
writable: false,
View
6 lib/sql.js
@@ -1,6 +1,10 @@
module.exports = BaseSQL;
-function BaseSQL() {}
+/**
+ * Base SQL class
+ */
+function BaseSQL() {
+}
BaseSQL.prototype.query = function () {
throw new Error('query method should be declared in adapter');
View
315 lib/validatable.js
@@ -1,92 +1,255 @@
exports.Validatable = Validatable;
+/**
+ * Validation encapsulated in this abstract class.
+ *
+ * Basically validation configurators is just class methods, which adds validations
+ * configs to AbstractClass._validations. Each of this validations run when
+ * `obj.isValid()` method called.
+ *
+ * Each configurator can accept n params (n-1 field names and one config). Config
+ * is {Object} depends on specific validation, but all of them has one common part:
+ * `message` member. It can be just string, when only one situation possible,
+ * e.g. `Post.validatesPresenceOf('title', { message: 'can not be blank' });`
+ *
+ * In more complicated cases it can be {Hash} of messages (for each case):
+ * `User.validatesLengthOf('password', { min: 6, max: 20, message: {min: 'too short', max: 'too long'}});`
+ */
function Validatable() {
// validatable class
};
+/**
+ * Validate presence. This validation fails when validated field is blank.
+ *
+ * Default error message "can't be blank"
+ *
+ * @example `Post.validatesPresenceOf('title')`
+ * @example `Post.validatesPresenceOf('title', {message: 'Can not be blank'})`
+ * @sync
+ *
+ * @nocode
+ * @see helper/validatePresence
+ */
Validatable.validatesPresenceOf = getConfigurator('presence');
+
+/**
+ * Validate length. Three kinds of validations: min, max, is.
+ *
+ * Default error messages:
+ *
+ * - min: too short
+ * - max: too long
+ * - is: length is wrong
+ *
+ * @example `User.validatesLengthOf('password', {min: 7});`
+ * @example `User.validatesLengthOf('email', {max: 100});`
+ * @example `User.validatesLengthOf('state', {is: 2});`
+ * @example `User.validatesLengthOf('nick', {min: 3, max: 15});
+ * @sync
+ *
+ * @nocode
+ * @see helper/validateLength
+ */
Validatable.validatesLengthOf = getConfigurator('length');
+
+/**
+ * Validate numericality.
+ *
+ * @example `User.validatesNumericalityOf('age', { message: { number: '...' }});`
+ * @example `User.validatesNumericalityOf('age', {int: true, message: { int: '...' }});`
+ *
+ * Default error messages:
+ *
+ * - number: is not a number
+ * - int: is not an integer
+ *
+ * @sync
+ *
+ * @nocode
+ * @see helper/validateNumericality
+ */
Validatable.validatesNumericalityOf = getConfigurator('numericality');
+
+/**
+ * Validate inclusion in set
+ *
+ * @example `User.validatesInclusionOf('gender', {in: ['male', 'female']});`
+ *
+ * Default error message: is not included in the list
+ *
+ * @nocode
+ * @see helper/validateInclusion
+ */
Validatable.validatesInclusionOf = getConfigurator('inclusion');
+
+/**
+ * Validate exclusion
+ *
+ * @example `Company.validatesExclusionOf('domain', {in: ['www', 'admin']});`
+ *
+ * Default error message: is reserved
+ *
+ * @nocode
+ * @see helper/validateExclusion
+ */
Validatable.validatesExclusionOf = getConfigurator('exclusion');
+
+/**
+ * Validate format
+ *
+ * Default error message: is invalid
+ *
+ * @nocode
+ * @see helper/validateFormat
+ */
Validatable.validatesFormatOf = getConfigurator('format');
+
+/**
+ * Validate using custom validator
+ *
+ * Default error message: is invalid
+ *
+ * @nocode
+ * @see helper/validateCustom
+ */
Validatable.validate = getConfigurator('custom');
+
+/**
+ * Validate using custom async validator
+ *
+ * Default error message: is invalid
+ *
+ * @async
+ * @nocode
+ * @see helper/validateCustom
+ */
Validatable.validateAsync = getConfigurator('custom', {async: true});
+
+/**
+ * Validate uniqueness
+ *
+ * Default error message: is not unique
+ *
+ * @async
+ * @nocode
+ * @see helper/validateUniqueness
+ */
Validatable.validatesUniquenessOf = getConfigurator('uniqueness', {async: true});
// implementation of validators
-var validators = {
- presence: function (attr, conf, err) {
- if (blank(this[attr])) {
- err();
- }
- },
- length: function (attr, conf, err) {
- if (nullCheck.call(this, attr, conf, err)) return;
- var len = this[attr].length;
- if (conf.min && len < conf.min) {
- err('min');
- }
- if (conf.max && len > conf.max) {
- err('max');
- }
- if (conf.is && len !== conf.is) {
- err('is');
- }
- },
- numericality: function (attr, conf, err) {
- if (nullCheck.call(this, attr, conf, err)) return;
+/**
+ * Presence validator
+ */
+function validatePresence(attr, conf, err) {
+ if (blank(this[attr])) {
+ err();
+ }
+}
- if (typeof this[attr] !== 'number') {
- return err('number');
- }
- if (conf.int && this[attr] !== Math.round(this[attr])) {
- return err('int');
- }
- },
- inclusion: function (attr, conf, err) {
- if (nullCheck.call(this, attr, conf, err)) return;
+/**
+ * Length validator
+ */
+function validateLength(attr, conf, err) {
+ if (nullCheck.call(this, attr, conf, err)) return;
- if (!~conf.in.indexOf(this[attr])) {
- err()
- }
- },
- exclusion: function (attr, conf, err) {
- if (nullCheck.call(this, attr, conf, err)) return;
+ var len = this[attr].length;
+ if (conf.min && len < conf.min) {
+ err('min');
+ }
+ if (conf.max && len > conf.max) {
+ err('max');
+ }
+ if (conf.is && len !== conf.is) {
+ err('is');
+ }
+}
- if (~conf.in.indexOf(this[attr])) {
- err()
- }
- },
- format: function (attr, conf, err) {
- if (nullCheck.call(this, attr, conf, err)) return;
+/**
+ * Numericality validator
+ */
+function validateNumericality(attr, conf, err) {
+ if (nullCheck.call(this, attr, conf, err)) return;
- if (typeof this[attr] === 'string') {
- if (!this[attr].match(conf['with'])) {
- err();
- }
- } else {
+ if (typeof this[attr] !== 'number') {
+ return err('number');
+ }
+ if (conf.int && this[attr] !== Math.round(this[attr])) {
+ return err('int');
+ }
+}
+
+/**
+ * Inclusion validator
+ */
+function validateInclusion(attr, conf, err) {
+ if (nullCheck.call(this, attr, conf, err)) return;
+
+ if (!~conf.in.indexOf(this[attr])) {
+ err()
+ }
+}
+
+/**
+ * Exclusion validator
+ */
+function validateExclusion(attr, conf, err) {
+ if (nullCheck.call(this, attr, conf, err)) return;
+
+ if (~conf.in.indexOf(this[attr])) {
+ err()
+ }
+}
+
+/**
+ * Format validator
+ */
+function validateFormat(attr, conf, err) {
+ if (nullCheck.call(this, attr, conf, err)) return;
+
+ if (typeof this[attr] === 'string') {
+ if (!this[attr].match(conf['with'])) {
err();
}
- },
- custom: function (attr, conf, err, done) {
- conf.customValidator.call(this, err, done);
- },
- uniqueness: function (attr, conf, err, done) {
- var cond = {where: {}};
- cond.where[attr] = this[attr];
- this.constructor.all(cond, function (error, found) {
- if (found.length > 1) {
- err();
- } else if (found.length === 1 && found[0].id !== this.id) {
- err();
- }
- done();
- }.bind(this));
+ } else {
+ err();
}
-};
+}
+
+/**
+ * Custom validator
+ */
+function validateCustom(attr, conf, err, done) {
+ conf.customValidator.call(this, err, done);
+}
+/**
+ * Uniqueness validator
+ */
+function validateUniqueness(attr, conf, err, done) {
+ var cond = {where: {}};
+ cond.where[attr] = this[attr];
+ this.constructor.all(cond, function (error, found) {
+ if (found.length > 1) {
+ err();
+ } else if (found.length === 1 && found[0].id !== this.id) {
+ err();
+ }
+ done();
+ }.bind(this));
+}
+
+var validators = {
+ presence: validatePresence,
+ length: validateLength,
+ numericality: validateNumericality,
+ inclusion: validateInclusion,
+ exclusion: validateExclusion,
+ format: validateFormat,
+ custom: validateCustom,
+ uniqueness: validateUniqueness
+};
function getConfigurator(name, opts) {
return function () {
@@ -94,6 +257,25 @@ function getConfigurator(name, opts) {
};
}
+/**
+ * This method performs validation, triggers validation hooks.
+ * Before validation `obj.errors` collection cleaned.
+ * Each validation can add errors to `obj.errors` collection.
+ * If collection is not blank, validation failed.
+ *
+ * @warning This method can be called as sync only when no async validation configured. It's strongly recommended to run all validations as asyncronous.
+ *
+ * @param {Function} callback called with (valid)
+ * @return {Boolean} true if no async validation configured and all passed
+ *
+ * @example ExpressJS controller: render user if valid, show flash otherwise
+ * ```
+ * user.isValid(function (valid) {
+ * if (valid) res.render({user: user});
+ * else res.flash('error', 'User is not valid'), console.log(user.errors), res.redirect('/users');
+ * });
+ * ```
+ */
Validatable.prototype.isValid = function (callback) {
var valid = true, inst = this, wait = 0, async = false;
@@ -269,6 +451,13 @@ function nullCheck(attr, conf, err) {
return false;
}
+/**
+ * Return true when v is undefined, blank array, null or empty string
+ * otherwise returns false
+ *
+ * @param {Mix} v
+ * @returns {Boolean} whether `v` blank or not
+ */
function blank(v) {
if (typeof v === 'undefined') return true;
if (v instanceof Array && v.length === 0) return true;
Please sign in to comment.
Something went wrong with that request. Please try again.