Revision 121df320d337fd860fed17f9bbddeb8e605427e2 authored by Aaron Heckmann on 09 February 2012, 13:20:07 UTC, committed by Aaron Heckmann on 09 February 2012, 13:20:07 UTC
1 parent 627745b
schema.js
/**
* Module dependencies.
*/
var EventEmitter = require('events').EventEmitter
, VirtualType = require('./virtualtype')
, utils = require('./utils')
, NamedScope
, Query
, Types
/**
* Schema constructor.
*
* @param {Object} definition
* @api public
*/
function Schema (obj, options) {
this.paths = {};
this.virtuals = {};
this.inherits = {};
this.callQueue = [];
this._indexes = [];
this.methods = {};
this.statics = {};
this.tree = {};
// set options
this.options = utils.options({
safe: true
, 'use$SetOnSave': true
, strict: false
}, options);
// build paths
if (obj)
this.add(obj);
if (!this.paths['_id'] && !this.options.noId) {
this.add({ _id: {type: ObjectId, auto: true} });
}
if (!this.paths['id'] && !this.options.noVirtualId) {
this.virtual('id').get(function () {
if (this.__id) {
return this.__id;
}
return this.__id = null == this._id
? null
: this._id.toString();
});
}
delete this.options.noVirtualId;
};
/**
* Inherit from EventEmitter.
*/
Schema.prototype.__proto__ = EventEmitter.prototype;
/**
* Schema by paths
*
* Example (embedded doc):
* {
* 'test' : SchemaType,
* , 'test.test' : SchemaType,
* , 'first_name' : SchemaType
* }
*
* @api private
*/
Schema.prototype.paths;
/**
* Schema as a tree
*
* Example:
* {
* '_id' : ObjectId
* , 'nested' : {
* 'key': String
* }
* }
*
* @api private
*/
Schema.prototype.tree;
/**
* Sets the keys
*
* @param {Object} keys
* @param {String} prefix
* @api public
*/
Schema.prototype.add = function add (obj, prefix) {
prefix = prefix || '';
for (var i in obj) {
if (null == obj[i]) {
throw new TypeError('Invalid value for schema path `'+ prefix + i +'`');
}
if (obj[i].constructor.name == 'Object' && (!obj[i].type || obj[i].type.type)) {
if (Object.keys(obj[i]).length)
this.add(obj[i], prefix + i + '.');
else
this.path(prefix + i, obj[i]); // mixed type
} else
this.path(prefix + i, obj[i]);
}
};
/**
* Sets a path (if arity 2)
* Gets a path (if arity 1)
*
* @param {String} path
* @param {Object} constructor
* @api public
*/
Schema.prototype.path = function (path, obj) {
if (obj == undefined) {
if (this.paths[path]) return this.paths[path];
// Sometimes path will come in as
// pathNameA.4.pathNameB where 4 means the index
// of an embedded document in an embedded array.
// In this case, we need to jump to the Array's
// schema and call path() from there to resolve to
// the correct path type
var last
, self = this
, subpaths = path.split(/\.(\d+)\.?/)
.filter(Boolean) // removes empty strings
if (subpaths.length > 1) {
last = subpaths.length - 1;
return subpaths.reduce(function (val, subpath, i) {
if (val && !val.schema) {
if (i === last && !/\D/.test(subpath) && val instanceof Types.Array) {
return val.caster; // StringSchema, NumberSchema, etc
} else {
return val;
}
}
if (!/\D/.test(subpath)) { // 'path.0.subpath' on path 0
return val;
}
return val ? val.schema.path(subpath)
: self.path(subpath);
}, null);
}
return this.paths[subpaths[0]];
}
// update the tree
var subpaths = path.split(/\./)
, last = subpaths.pop()
, branch = this.tree;
subpaths.forEach(function(path) {
if (!branch[path]) branch[path] = {};
branch = branch[path];
});
branch[last] = utils.clone(obj);
this.paths[path] = Schema.interpretAsType(path, obj);
return this;
};
/**
* Converts -- e.g., Number, [SomeSchema],
* { type: String, enum: ['m', 'f'] } -- into
* the appropriate Mongoose Type, which we use
* later for casting, validation, etc.
* @param {String} path
* @param {Object} constructor
*/
Schema.interpretAsType = function (path, obj) {
if (obj.constructor.name != 'Object')
obj = { type: obj };
// Get the type making sure to allow keys named "type"
// and default to mixed if not specified.
// { type: { type: String, default: 'freshcut' } }
var type = obj.type && !obj.type.type
? obj.type
: {};
if (type.constructor.name == 'Object') {
return new Types.Mixed(path, obj);
}
if (Array.isArray(type) || type == Array) {
// if it was specified through { type } look for `cast`
var cast = type == Array
? obj.cast
: type[0];
if (cast instanceof Schema) {
return new Types.DocumentArray(path, cast, obj);
}
return new Types.Array(path, cast || Types.Mixed, obj);
}
if (undefined == Types[type.name]) {
throw new TypeError('Undefined type at `' + path +
'`\n Did you try nesting Schemas? ' +
'You can only nest using refs or arrays.');
}
return new Types[type.name](path, obj);
};
/**
* Iterates through the schema's paths, passing the path string and type object
* to the callback.
*
* @param {Function} callback function - fn(pathstring, type)
* @return {Schema} this for chaining
* @api public
*/
Schema.prototype.eachPath = function (fn) {
var keys = Object.keys(this.paths)
, len = keys.length;
for (var i = 0; i < len; ++i) {
fn(keys[i], this.paths[keys[i]]);
}
return this;
};
/**
* Returns an Array of path strings that are required.
* @api public
*/
Object.defineProperty(Schema.prototype, 'requiredPaths', {
get: function () {
var paths = this.paths
, pathnames = Object.keys(paths)
, i = pathnames.length
, pathname, path
, requiredPaths = [];
while (i--) {
pathname = pathnames[i];
path = paths[pathname];
if (path.isRequired) requiredPaths.push(pathname);
}
return requiredPaths;
}
});
/**
* Given a path, returns whether it is a real, virtual, or
* ad-hoc/undefined path
*
* @param {String} path
* @return {String}
* @api public
*/
Schema.prototype.pathType = function (path) {
if (path in this.paths) return 'real';
if (path in this.virtuals) return 'virtual';
return 'adhocOrUndefined';
};
/**
* Adds a method call to the queue
*
* @param {String} method name
* @param {Array} arguments
* @api private
*/
Schema.prototype.queue = function(name, args){
this.callQueue.push([name, args]);
return this;
};
/**
* Defines a pre for the document
*
* @param {String} method
* @param {Function} callback
* @api public
*/
Schema.prototype.pre = function(){
return this.queue('pre', arguments);
};
/**
* Defines a post for the document
*
* @param {String} method
* @param {Function} callback
* @api public
*/
Schema.prototype.post = function(method, fn){
return this.queue('on', arguments);
};
/**
* Registers a plugin for this schema
*
* @param {Function} plugin callback
* @api public
*/
Schema.prototype.plugin = function (fn, opts) {
fn(this, opts);
return this;
};
/**
* Adds a method
*
* @param {String} method name
* @param {Function} handler
* @api public
*/
Schema.prototype.method = function (name, fn) {
if ('string' != typeof name)
for (var i in name)
this.methods[i] = name[i];
else
this.methods[name] = fn;
return this;
};
/**
* Defines a static method
*
* @param {String} name
* @param {Function} handler
* @api public
*/
Schema.prototype.static = function(name, fn) {
if ('string' != typeof name)
for (var i in name)
this.statics[i] = name[i];
else
this.statics[name] = fn;
return this;
};
/**
* Defines an index (most likely compound)
* Example:
* schema.index({ first: 1, last: -1 })
*
* @param {Object} field
* @param {Object} optional options object
* @api public
*/
Schema.prototype.index = function (fields, options) {
this._indexes.push([fields, options || {}]);
return this;
};
/**
* Sets/gets an option
*
* @param {String} key
* @param {Object} optional value
* @api public
*/
Schema.prototype.set = function (key, value) {
if (arguments.length == 1)
return this.options[key];
this.options[key] = value;
return this;
};
/**
* Compiles indexes from fields and schema-level indexes
*
* @api public
*/
Schema.prototype.__defineGetter__('indexes', function () {
var indexes = []
, seenSchemas = [];
collectIndexes(this);
return indexes;
function collectIndexes (schema, prefix) {
if (~seenSchemas.indexOf(schema)) return;
seenSchemas.push(schema);
var index;
var paths = schema.paths;
prefix = prefix || '';
for (var i in paths) {
if (paths[i]) {
if (paths[i] instanceof Types.DocumentArray) {
collectIndexes(paths[i].schema, i + '.');
} else {
index = paths[i]._index;
if (index !== false && index !== null){
var field = {};
field[prefix + i] = '2d' === index ? index : 1;
indexes.push([field, 'Object' === index.constructor.name ? index : {} ]);
}
}
}
}
if (prefix) {
fixSubIndexPaths(schema, prefix);
} else {
indexes = indexes.concat(schema._indexes);
}
}
/**
* Checks for indexes added to subdocs using Schema.index().
* These indexes need their paths prefixed properly.
*
* schema._indexes = [ [indexObj, options], [indexObj, options] ..]
*/
function fixSubIndexPaths (schema, prefix) {
var subindexes = schema._indexes
, len = subindexes.length
, indexObj
, newindex
, klen
, keys
, key
, i = 0
, j
for (i = 0; i < len; ++i) {
indexObj = subindexes[i][0];
keys = Object.keys(indexObj);
klen = keys.length;
newindex = {};
// use forward iteration, order matters
for (j = 0; j < klen; ++j) {
key = keys[j];
newindex[prefix + key] = indexObj[key];
}
indexes.push([newindex, subindexes[i][1]]);
}
}
});
/**
* Retrieves or creates the virtual type with the given name.
*
* @param {String} name
* @return {VirtualType}
*/
Schema.prototype.virtual = function (name, options) {
var virtuals = this.virtuals || (this.virtuals = {});
var parts = name.split('.');
return virtuals[name] = parts.reduce(function (mem, part, i) {
mem[part] || (mem[part] = (i === parts.length-1)
? new VirtualType(options)
: {});
return mem[part];
}, this.tree);
};
/**
* Fetches the virtual type with the given name.
* Should be distinct from virtual because virtual auto-defines a new VirtualType
* if the path doesn't exist.
*
* @param {String} name
* @return {VirtualType}
*/
Schema.prototype.virtualpath = function (name) {
return this.virtuals[name];
};
Schema.prototype.namedScope = function (name, fn) {
var namedScopes = this.namedScopes || (this.namedScopes = new NamedScope)
, newScope = Object.create(namedScopes)
, allScopes = namedScopes.scopesByName || (namedScopes.scopesByName = {});
allScopes[name] = newScope;
newScope.name = name;
newScope.block = fn;
newScope.query = new Query();
newScope.decorate(namedScopes, {
block0: function (block) {
return function () {
block.call(this.query);
return this;
};
},
blockN: function (block) {
return function () {
block.apply(this.query, arguments);
return this;
};
},
basic: function (query) {
return function () {
this.query.find(query);
return this;
};
}
});
return newScope;
};
/**
* ObjectId schema identifier. Not an actual ObjectId, only used for Schemas.
*
* @api public
*/
function ObjectId () {
throw new Error('This is an abstract interface. Its only purpose is to mark '
+ 'fields as ObjectId in the schema creation.');
}
/**
* Module exports.
*/
module.exports = exports = Schema;
// require down here because of reference issues
exports.Types = Types = require('./schema/index');
NamedScope = require('./namedscope')
Query = require('./query');
exports.ObjectId = ObjectId;
Computing file changes ...