lib/index.js
/*jshint loopfunc:true*/
var container = require('./config').container;
var errorPrefix = 'reheat.defineModel(name, staticProps[, protoProps]): ';
var errorPrefix2 = 'reheat.defineCollection(name, staticProps[, protoProps]): ';
/**
* @doc interface
* @id reheat
* @name reheat
*/
var reheat = module.exports = {};
container.resolve(function (Promise, robocop, utils, errors, extend, Model, Collection, Connection, models, collections) {
function evaluateRelations() {
utils.forOwn(models, function (model, modelName) {
if ('hasOne' in model.relations && !utils.isObject(model.relations.hasOne)) {
throw new errors.IllegalArgumentError(errorPrefix + modelName + '.relations.hasOne: Must be an object!', { actual: typeof model.relations.hasOne, expected: 'object' });
} else if ('belongsTo' in model.relations && !utils.isObject(model.relations.belongsTo)) {
throw new errors.IllegalArgumentError(errorPrefix + modelName + '.relations.belongsTo: Must be an object!', { actual: typeof model.relations.belongsTo, expected: 'object' });
} else if ('hasMany' in model.relations && !utils.isObject(model.relations.hasMany)) {
throw new errors.IllegalArgumentError(errorPrefix + modelName + '.relations.hasMany: Must be an object!', { actual: typeof model.relations.hasMany, expected: 'object' });
}
model.relations.hasOne = model.relations.hasOne || {};
model.relations.belongsTo = model.relations.belongsTo || {};
model.relations.hasMany = model.relations.hasMany || {};
utils.forOwn(model.relations.hasOne, function (relation, relationModelName) {
if (!utils.isObject(relation)) {
throw new errors.IllegalArgumentError(errorPrefix + modelName + '.relations.hasOne.' + relationModelName + ': Must be an object!', { actual: typeof relation, expected: 'object' });
} else if ('localField' in relation && !utils.isString(relation.localField)) {
throw new errors.IllegalArgumentError(errorPrefix + modelName + '.relations.hasOne.' + relationModelName + '.localField: Must be a string!', { actual: typeof relation.localField, expected: 'string' });
} else if ('foreignKey' in relation && !utils.isString(relation.foreignKey)) {
throw new errors.IllegalArgumentError(errorPrefix + modelName + '.relations.hasOne.' + relationModelName + '.foreignKey: Must be a string!', { actual: typeof relation.foreignKey, expected: 'string' });
}
if (!('localField' in relation)) {
relation.localField = utils.camelCase(relationModelName);
}
if (!('foreignKey' in relation)) {
relation.foreignKey = utils.camelCase(relationModelName) + 'Id';
}
if (models[relationModelName] && !models[relationModelName].relations.indices[relation.foreignKey]) {
models[relationModelName].relations.indices[relation.foreignKey] = null;
}
});
utils.forOwn(model.relations.belongsTo, function (relation, relationModelName) {
if (!utils.isObject(relation)) {
throw new errors.IllegalArgumentError(errorPrefix + modelName + '.relations.' + relationModelName + ': Must be an object!', { actual: typeof relation, expected: 'object' });
} else if ('localField' in relation && !utils.isString(relation.localField)) {
throw new errors.IllegalArgumentError(errorPrefix + modelName + '.relations.belongsTo.' + relationModelName + '.localField: Must be a string!', { actual: typeof relation.localField, expected: 'string' });
} else if ('localKey' in relation && !utils.isString(relation.localKey)) {
throw new errors.IllegalArgumentError(errorPrefix + modelName + '.relations.belongsTo.' + relationModelName + '.localKey: Must be a string!', { actual: typeof relation.localKey, expected: 'string' });
}
if (!('localField' in relation)) {
relation.localField = utils.camelCase(relationModelName);
}
if (!('localKey' in relation)) {
relation.localKey = utils.camelCase(relationModelName) + 'Id';
}
models[modelName].relations.indices[relation.localKey] = models[modelName].relations.indices[relation.localKey] || null;
});
utils.forOwn(model.relations.hasMany, function (relation, relationModelName) {
if (!utils.isObject(relation)) {
throw new errors.IllegalArgumentError(errorPrefix + modelName + '.relations.' + relationModelName + ': Must be an object!', { actual: typeof relation, expected: 'object' });
} else if ('localField' in relation && !utils.isString(relation.localField)) {
throw new errors.IllegalArgumentError(errorPrefix + modelName + '.relations.hasMany.' + relationModelName + '.localField: Must be a string!', { actual: typeof relation.localField, expected: 'string' });
} else if ('foreignKey' in relation && !utils.isString(relation.foreignKey)) {
throw new errors.IllegalArgumentError(errorPrefix + modelName + '.relations.hasMany.' + relationModelName + '.foreignKey: Must be a string!', { actual: typeof relation.foreignKey, expected: 'string' });
}
if (!('localField' in relation)) {
relation.localField = utils.camelCase(relationModelName) + 'List';
}
if (!('foreignKey' in relation)) {
relation.foreignKey = utils.camelCase(relationModelName) + 'Id';
}
if (models[relationModelName] && !models[relationModelName].relations.indices[relation.foreignKey]) {
models[relationModelName].relations.indices[relation.foreignKey] = null;
}
});
utils.forOwn(model.relations.indices, function (relationModelName, index) {
if (!model.relations.indices[index]) {
(function (m, i) {
m.relations.indices[i] = Promise.resolve().bind(m)
.then(function () {
if (this.tableReady && this.tableReady !== true) {
return this.tableReady;
}
})
.then(function () {
var r = this.r;
return r.branch(r.table(this.tableName).indexList().contains(i), null, r.table(this.tableName).indexCreate(i)).run();
})
.finally(function () {
this.relations.indices[i] = true;
});
})(model, index);
}
});
});
}
/**
* @doc property
* @id reheat.properties:Connection
* @name Connection
*/
reheat.Connection = Connection;
/**
* @doc interface
* @id reheat.properties:support
* @name support
*/
reheat.support = {
/**
* @doc property
* @id reheat.properties:support.UnhandledError
* @name UnhandledError
* @propertyOf reheat.properties:support
* @description
* See [UnhandledError](/documentation/api/api/support.error_types:UnhandledError).
*/
UnhandledError: errors.UnhandledError,
/**
* @doc property
* @id reheat.properties:support.IllegalArgumentError
* @name IllegalArgumentError
* @propertyOf reheat.properties:support
* @description
* See [IllegalArgumentError](/documentation/api/api/support.error_types:IllegalArgumentError).
*/
IllegalArgumentError: errors.IllegalArgumentError,
/**
* @doc property
* @id reheat.properties:support.RuntimeError
* @name RuntimeError
* @propertyOf reheat.properties:support
* @description
* See [RuntimeError](/documentation/api/api/support.error_types:RuntimeError).
*/
RuntimeError: errors.RuntimeError,
/**
* @doc property
* @id reheat.properties:support.ValidationError
* @name ValidationError
* @propertyOf reheat.properties:support
* @description
* See [ValidationError](/documentation/api/api/support.error_types:ValidationError).
*/
ValidationError: errors.ValidationError
};
/**
* @doc method
* @id reheat.methods:defineModel
* @name defineModel
* @description
* Register a new Model with reheat.
*
* ## Signature:
* ```js
* reheat.defineModel(name[, staticProperties][, prototypeProperties])
* ```
*
* ## Example:
*
* ```js
* var reheat = require('reheat'),
* connection = new reheat.Connection();
*
* var Post = reheat.defineModel('Post', {
* connection: connection,
* tableName: 'post',
* softDelete: true
* }, {
* beforeCreate: function(cb) {
* console.log('before create lifecycle step!');
* cb();
* }
* }),
* Posts = Post.collection;
*
* // All prototype properties and methods will be available on instances of Post.
* var post = new Post();
*
* // All static properties and methods will be available on Post itself.
* Post.tableName; // 'post'
* Post.idAttribute; // 'id'
* Post.connection.run(r.tableList(), function (err, tables) {});
* Posts.findAll({}, function (err, posts) {
* posts; // All posts in the "post" table
* });
* ```
*
* @param {string} name The name of the new model.
* @param {object} staticProps Properties and methods to be added as static properties of the child class. See
* Model for static properties and methods. Static methods should not be overridden. Some static properties
* have defaults, others are required to be set by the developer, like `Model.connection`. You can add any
* static properties and methods you want as long as they don't conflict with already existing static properties and
* methods. Properties:
*
* - `{string="test"}` - tableName - The name of the table this model should map to.
* - `{string="id"}` - idAttribute - The field that specifies the primary key for instances of this model.
* - `{boolean=false}` - softDelete - Whether to add a `deleted` timestamp field to rows instead of deleting them.
* - `{boolean=false}` - timestamps - Whether reheat should manage timestamps for instances of this model.
* - `{Connection}` - connection - Instance of `reheat.Connection` this model should use.
* - `{Schema=}` - schema - Schema this model should use.
*
* @param {object=} protoProps Properties and methods to be added to the prototype of the child class. See
* Model for default prototype properties and methods. Prototype properties and methods can be overridden for
* custom behavior. Properties:
*
* - `{function=}` - `beforeValidate(cb)`
* - `{function=}` - `validate(cb)`
* - `{function=}` - `afterValidate(cb)`
* - `{function=}` - `beforeCreate(cb)`
* - `{function=}` - `afterCreate(instance, cb)`
* - `{function=}` - `beforeUpdate(cb)`
* - `{function=}` - `afterUpdate(instance, cb)`
* - `{function=}` - `beforeDestroy(cb)`
* - `{function=}` - `afterDestroy(instance, cb)`
*
* @returns {Model} model The newly registered Model.
*/
reheat.defineModel = function (name, staticProps, protoProps) {
if (!utils.isString(name)) {
throw new errors.IllegalArgumentError(errorPrefix + 'name: Must be a string!', { name: { actual: typeof name, expected: 'string' } });
} else if (models[name]) {
throw new errors.RuntimeError(errorPrefix + 'name: A Model with that name already exists!');
} else if ('idAttribute' in staticProps && !utils.isString(staticProps.idAttribute)) {
throw new errors.IllegalArgumentError(errorPrefix + 'staticProps.idAttribute: Must be a string!', { idAttribute: { actual: typeof staticProps.idAttribute, expected: 'string' } });
} else if ('tableName' in staticProps && !utils.isString(staticProps.tableName)) {
throw new errors.IllegalArgumentError(errorPrefix + 'staticProps.tableName: Must be a string!', { tableName: { actual: typeof staticProps.tableName, expected: 'string' } });
} else if ('timestamps' in staticProps && !utils.isBoolean(staticProps.timestamps)) {
throw new errors.IllegalArgumentError(errorPrefix + 'staticProps.timestamps: Must be a boolean!', { timestamps: { actual: typeof staticProps.timestamps, expected: 'string' } });
} else if ('softDelete' in staticProps && !utils.isBoolean(staticProps.softDelete)) {
throw new errors.IllegalArgumentError(errorPrefix + 'staticProps.softDelete: Must be a boolean!', { softDelete: { actual: typeof staticProps.softDelete, expected: 'string' } });
} else if (!(staticProps.connection instanceof Connection)) {
throw new errors.IllegalArgumentError(errorPrefix + 'staticProps.connection: Must be an instance of Connection!', { connection: { actual: typeof staticProps.connection, expected: 'Connection' } });
} else if ('schema' in staticProps && staticProps.schema && !utils.isObject(staticProps.schema)) {
throw new errors.IllegalArgumentError(errorPrefix + 'staticProps.schema: Must be an object!', { schema: { actual: typeof staticProps.schema, expected: 'object' } });
} else if ('schema' in staticProps && staticProps.schema && !utils.isFunction(staticProps.schema.validate)) {
throw new errors.IllegalArgumentError(errorPrefix + 'staticProps.schema.validate: Must be a function!', { schema: { validate: { actual: typeof staticProps.schema.validate, expected: 'function' } } });
} else if ('relations' in staticProps && !utils.isObject(staticProps.relations)) {
throw new errors.IllegalArgumentError(errorPrefix + 'staticProps.relations: Must be an object!', { relations: { actual: typeof staticProps.relations, expected: 'object' } });
} else {
// Infer default tableName from model name
if (!('tableName' in staticProps)) {
staticProps.tableName = utils.lowerCase(name);
}
staticProps.relations = staticProps.relations || {};
staticProps.relations.indices = staticProps.relations.indices || {};
staticProps.modelName = name;
models[name] = extend.apply(Model, [protoProps, staticProps]);
var r = models[name].connection.r;
models[name].r = r;
// Ensure table exists
models[name].tableReady = r.branch(r.tableList().contains(models[name].tableName), null, r.tableCreate(models[name].tableName)).run()
.finally(function () {
models[name].tableReady = true;
});
try {
evaluateRelations();
} catch (err) {
delete models[name];
throw err;
}
models[name].collection = extend.apply(Collection, [
{},
{
model: Model,
collectionName: Model.modelName + 'Collection'
}
]);
models[name].collection.model = models[name];
return models[name];
}
};
/**
* @doc method
* @id reheat.methods:unregisterModel
* @name unregisterModel
* @description
* Unregister the model with the given name from reheat's registry.
*
* ## Signature:
* ```js
* reheat.unregisterModel(name)
* ```
*
* @param {string} name The name of the model to unregister.
*/
reheat.unregisterModel = function (name) {
if (models[name] && models[name].collection) {
models[name].collection.model = null;
delete collections[models[name].collection.collectionName];
models[name].collection = null;
}
delete models[name];
};
/**
* @doc method
* @id reheat.methods:defineCollection
* @name defineCollection
* @description
* Register a new Collection with reheat. This is optional. A default collection will be created for every model you define.
*
* ## Signature:
* ```js
* reheat.defineCollection(name[, staticProperties][, prototypeProperties])
* ```
*
* ## Example:
*
* ```js
* var reheat = require('reheat'),
* Post = require('../models/Post');
*
* var Post = reheat.defineCollection('Posts, {
* model: Post
* }, {
* something: function(cb) {
* console.log('something');
* cb();
* }
* });
*
* // All prototype properties and methods will be available on instances of Post.
* var posts = new Posts([
* {
* author: 'John Anderson',
* title: 'How NOT to cook'
* },
* {
* author: 'Sally Johnson',
* title: 'How to cook'
* }
* ]);
* ```
*
* @param {string} name The name of the new collection.
* @param {object} staticProps Properties and methods to be added as static properties of the child class. See
* Collection for static properties and methods. Static methods should not be overridden. Some static properties
* have defaults, others are required to be set by the developer, like `Collection.model`. You can add any
* static properties and methods you want as long as they don't conflict with already existing static properties and
* methods. Properties:
*
* - `{string="test"}` - model - The Model of this Collection.
*
* @param {object=} protoProps Properties and methods to be added to the prototype of the child class. See
* Collection for default prototype properties and methods. Prototype properties and methods can be overridden for
* custom behavior. Properties:
*
* - `{function=}` - `something(cb)`
*
* @returns {Collection} collection The newly registered Collection.
*/
reheat.defineCollection = function (name, staticProps, protoProps) {
if (!utils.isString(name)) {
throw new errors.IllegalArgumentError(errorPrefix2 + 'name: Must be a string!', { name: { actual: typeof name, expected: 'string' } });
} else if (collections[name]) {
throw new errors.RuntimeError(errorPrefix2 + 'name: A Collection with that name already exists!');
} else if (!staticProps.model.__reheat_super__) {
throw new errors.IllegalArgumentError(errorPrefix2 + 'staticProps.model: Must be a subclass of Model!', { model: { actual: typeof staticProps.model, expected: 'subclass of Model' } });
} else {
staticProps.collectionName = name;
collections[name] = extend.apply(Collection, [protoProps, staticProps]);
collections[name].model.collection = collections[name];
return collections[name];
}
};
/**
* @doc method
* @id reheat.methods:unregisterModel
* @name unregisterModel
* @description
* Unregister the model with the given name from reheat's registry.
*
* ## Signature:
* ```js
* reheat.unregisterCollection(name)
* ```
*
* @param {string} name The name of the model to unregister.
*/
reheat.unregisterCollection = function (name) {
if (collections[name]) {
collections[name].model.collection = extend.apply(Collection, [
{},
{
model: collections[name].model,
collectionName: collections[name].model.modelName + 'Collection'
}
]);
}
delete collections[name];
};
/**
* @doc method
* @id reheat.methods:getModel
* @name getModel
* @description
* Retrieve the model with the given name from reheat's registry.
*
* ## Signature:
* ```js
* reheat.getModel(name)
* ```
*
* @param {string} name The name of the model to retrieve.
* @returns {object} The model with the given name;
*/
reheat.getModel = function (name) {
return models[name];
};
/**
* @doc method
* @id reheat.methods:getCollection
* @name getCollection
* @description
* Retrieve the collection with the given name from reheat's registry.
*
* ## Signature:
* ```js
* reheat.getCollection(name)
* ```
*
* @param {string} name The name of the collection to retrieve.
* @returns {object} The collection with the given name;
*/
reheat.getCollection = function (name) {
return collections[name];
};
/**
* @doc method
* @id reheat.methods:availableDataTypes
* @name availableDataTypes
* @description
* See [robocop.availableDataTypes](http://jmdobry.github.io/robocop.js/api.html#robocopavailabledatatypes).
*/
/**
* @doc method
* @id reheat.methods:availableRules
* @name availableRules
* @description
* See [robocop.availableRules](http://jmdobry.github.io/robocop.js/api.html#robocopavailablerules).
*/
/**
* @doc method
* @id reheat.methods:availableSchemas
* @name availableSchemas
* @description
* See [robocop.availableSchemas](http://jmdobry.github.io/robocop.js/api.html#robocopavailableschemas).
*/
/**
* @doc method
* @id reheat.methods:defineDataType
* @name defineDataType
* @description
* See [robocop.defineDataType](http://jmdobry.github.io/robocop.js/api.html#robocopdefinedatatype).
*/
/**
* @doc method
* @id reheat.methods:defineRule
* @name defineRule
* @description
* See [robocop.defineRule](http://jmdobry.github.io/robocop.js/api.html#robocopdefinerule).
*/
/**
* @doc method
* @id reheat.methods:defineSchema
* @name defineSchema
* @description
* See [robocop.defineSchema](http://jmdobry.github.io/robocop.js/api.html#robocopdefineschema).
*/
/**
* @doc method
* @id reheat.methods:getDataType
* @name getDataType
* @description
* See [robocop.getDataType](http://jmdobry.github.io/robocop.js/api.html#robocopgetdatatype).
*/
/**
* @doc method
* @id reheat.methods:getRule
* @name getRule
* @description
* See [robocop.getRule](http://jmdobry.github.io/robocop.js/api.html#robocopgetrule).
*/
/**
* @doc method
* @id reheat.methods:getSchema
* @name getSchema
* @description
* See [robocop.getSchema](http://jmdobry.github.io/robocop.js/api.html#robocopgetschema).
*/
/**
* @doc method
* @id reheat.methods:removeDataType
* @name removeDataType
* @description
* See [robocop.removeDataType](http://jmdobry.github.io/robocop.js/api.html#robocopremovedatatype).
*/
/**
* @doc method
* @id reheat.methods:removeRule
* @name removeRule
* @description
* See [robocop.removeRule](http://jmdobry.github.io/robocop.js/api.html#robocopremoverule).
*/
/**
* @doc method
* @id reheat.methods:removeSchema
* @name removeSchema
* @description
* See [robocop.removeSchema](http://jmdobry.github.io/robocop.js/api.html#robocopremoveschema).
*/
utils.deepMixIn(reheat, robocop);
delete reheat.Schema;
// Freeze the API
utils.deepFreeze(reheat);
});