UNPKG

reheat

Version:

A red hot Node.js ORM for RethinkDB.

547 lines (515 loc) 22.4 kB
/*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); });