Permalink
Browse files

Added tests and implementation for on-the-fly schemas.

  • Loading branch information...
bnoguchi committed May 5, 2011
1 parent 99e6a56 commit 8e492e59f4cfa4204899fb40245c717107b2c739
Showing with 192 additions and 42 deletions.
  1. +36 −9 lib/mongoose/document.js
  2. +2 −1 lib/mongoose/model.js
  3. +34 −25 lib/mongoose/schema.js
  4. +18 −6 lib/mongoose/schematype.js
  5. +0 −1 test/model.test.js
  6. +102 −0 test/schema.onthefly.test.js
View
@@ -6,6 +6,7 @@
var EventEmitter = require('events').EventEmitter
, MongooseError = require('./error')
, MixedSchema = require('./schema/mixed')
+ , Schema = require('./schema')
, utils = require('./utils')
, clone = utils.clone
, ActiveRoster = utils.StateMachine.ctor('require', 'modify', 'init')
@@ -182,13 +183,15 @@ Document.prototype.pre = function (method, fn) {
*
* @param {String/Object} key path, or object
* @param {Object} value, or undefined or a prefix if first parameter is an object
- * @param {Boolean} whether to apply transformations: cast, setters (true)
- * @param {Boolean} whether to mark dirty (true)
- * @param {Boolean} whether this is an initialization
+ * @param @optional {Schema|String|...} specify a type if this is an on-the-fly attribute
* @api public
*/
-Document.prototype.set = function (path, val) {
+Document.prototype.set = function (path, val, type) {
+ if (type) {
+ adhocs = this._adhocPaths || (this._adhocPaths = {});
+ adhocs[path] = Schema.interpretAsType(path, type);
+ }
if ('string' !== typeof path) {
if (null === path || undefined === path)
@@ -204,7 +207,7 @@ Document.prototype.set = function (path, val) {
while (i--) {
key = keys[i];
- if (!(this.schema.path(prefix + key) instanceof MixedSchema)
+ if (!(this.path(prefix + key) instanceof MixedSchema)
&& undefined !== path[key]
&& null !== path[key]
&& Object == path[key].constructor) {
@@ -217,7 +220,7 @@ Document.prototype.set = function (path, val) {
return this;
}
- var schema = this.schema.path(path)
+ var schema = this.path(path)
, parts = path.split('.')
, obj = this.doc
, self = this;
@@ -345,25 +348,49 @@ Document.prototype.doCast = function (path) {
* Gets a path
*
* @param {String} key path
+ * @param @optional {Schema|String|...} specify a type if this is an on-the-fly attribute
* @api public
*/
-Document.prototype.get = function (path) {
+Document.prototype.get = function (path, type) {
+ if (type) {
+ adhocs = this._adhocPaths || (this._adhocPaths = {});
+ adhocs[path] = Schema.interpretAsType(path, type);
+ }
var obj
- , schema = this.schema.path(path) || this.schema.virtualpath(path)
+ , schema = this.path(path) || this.schema.virtualpath(path)
+ , adhocs
, pieces = path.split('.');
obj = this.doc;
for (var i = 0, l = pieces.length; i < l; i++)
obj = null === obj ? null : obj[pieces[i]];
- if (schema)
+ if (schema) {
obj = schema.applyGetters(obj, this);
+ }
// TODO Cache obj
return obj;
};
+/**
+ * Finds the path in the ad hoc type schema list or
+ * in the schema's list of type schemas
+ * @param {String} path
+ * @param {Object} obj
+ */
+Document.prototype.path = function (path, obj) {
+ var adhocs = this._adhocPaths
+ , adhocType = adhocs && adhocs[path];
+ if (adhocType) {
+ return adhocType;
+ } else {
+ return this.schema.path(path);
+ }
+};
+
+
/**
* Commits a path, marking as modified if needed. Useful for mixed keys
*
View
@@ -81,7 +81,7 @@ Model.prototype.save = function (fn) {
var dirty = this.activePaths.map('modify', function (path) {
return { path: path
, value: self.getValue(path)
- , schema: self.schema.path(path) };
+ , schema: self.path(path) };
});
if (this.isNew) {
@@ -133,6 +133,7 @@ Model.prototype.save = function (fn) {
if (op === '$pull' || op === '$push') {
if (val.constructor !== Object) {
if (Array.isArray(val)) val = [val];
+ // TODO Should we place pull and push casting into the pull and push methods?
val = schema.cast(val)[0];
}
}
View
@@ -4,7 +4,7 @@
*/
var EventEmitter = require('events').EventEmitter
- , Types = require('./schema/index')
+ , Types
, VirtualType = require('./virtualtype')
, utils = require('./utils')
, NamedScope = require('./namedscope')
@@ -128,22 +128,30 @@ Schema.prototype.path = function (path, obj) {
// the correct path type (otherwise, it falsely
// resolves to undefined
var self = this
- , subpaths = path.split(/\.\d+\./)
- , val;
+ , subpaths = path.split(/\.\d+\./);
if (subpaths.length > 1) {
- val = subpaths.reduce( function (val, subpath) {
- if (val) {
- return val.schema.path(subpath);
- } else {
- return self.path(subpath);
- }
- }, null);
- } else {
- return this.paths[path];
+ return subpaths.reduce( function (val, subpath) {
+ return val ? val.schema.path(subpath)
+ : self.path(subpath);
+ }, null);
}
- return val;
+ return; // Otherwise, return `undefined`
}
+ 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 != Object)
obj = { type: obj };
@@ -154,24 +162,24 @@ Schema.prototype.path = function (path, obj) {
? obj.type
: {};
- if (type.constructor == Object)
- this.paths[path] = new Types.Mixed(path, obj);
- else if (Array.isArray(type) || type == Array){
+ if (type.constructor == 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];
cast = cast || Types.Mixed;
- if (cast instanceof Schema)
- this.paths[path] = new Types.DocumentArray(path, cast, obj);
- else
- this.paths[path] = new Types.Array(path, cast, obj);
- } else
- this.paths[path] = new Types[type.name](path, obj);
-
- return this;
+ if (cast instanceof Schema) {
+ return new Types.DocumentArray(path, cast, obj);
+ }
+ return new Types.Array(path, cast, obj);
+ }
+ return new Types[type.name](path, obj);
};
/**
@@ -461,6 +469,7 @@ function ObjectId () {
module.exports = exports = Schema;
-exports.Types = Types;
+// require down here because of reference issues
+exports.Types = Types = require('./schema/index');
exports.ObjectId = ObjectId;
View
@@ -177,9 +177,15 @@ SchemaType.prototype.getDefault = function (scope) {
*/
SchemaType.prototype.applySetters = function (value, scope) {
- var v = value;
- for (var l = this.setters.length - 1; l >= 0; l--){
- v = this.setters[l].call(scope, v);
+ var v = value
+ , setters = this.setters
+ , l = setters.length;
+ for (k = l - 1; k >= 0; k--){
+ v = setters[k].call(scope, v);
+ if (v === null || v === undefined) return v;
+ v = this.cast(v, scope);
+ }
+ if (!l) {
if (v === null || v === undefined) return v;
v = this.cast(v, scope);
}
@@ -195,9 +201,15 @@ SchemaType.prototype.applySetters = function (value, scope) {
*/
SchemaType.prototype.applyGetters = function (value, scope) {
- var v = value;
- for (var l = this.getters.length - 1; l >= 0; l--){
- v = this.getters[l].call(scope, v);
+ var v = value
+ , getters = this.getters
+ , l = getters.length;
+ for (var k = l - 1; k >= 0; k--){
+ v = this.getters[k].call(scope, v);
+ if (v === null || v === undefined) return v;
+ v = this.cast(v, scope);
+ }
+ if (!l) {
if (v === null || v === undefined) return v;
v = this.cast(v, scope);
}
View
@@ -3281,5 +3281,4 @@ module.exports = {
});
})
}
-
};
@@ -0,0 +1,102 @@
+var start = require('./common')
+ , should = require('should')
+ , mongoose = start.mongoose
+ , random = require('mongoose/utils').random
+ , Schema = mongoose.Schema
+ , ObjectId = Schema.ObjectId;
+
+/**
+ * Setup.
+ */
+
+var DecoratedSchema = new Schema({
+ title : String
+});
+
+mongoose.model('Decorated', DecoratedSchema);
+
+var collection = 'decorated_' + random();
+
+module.exports = {
+ 'setting on the fly schemas should cache the type schema and cast values appropriately': function () {
+ var db = start()
+ , Decorated = db.model('Decorated', collection);
+
+ var post = new Decorated();
+ post.set('adhoc', '9', Number);
+ post.get('adhoc').valueOf().should.eql(9);
+ db.close();
+ },
+
+ 'on the fly schemas should be local to the particular document': function () {
+ var db = start()
+ , Decorated = db.model('Decorated', collection);
+
+ var postOne = new Decorated();
+ postOne.set('adhoc', '9', Number);
+ postOne.path('adhoc').should.not.equal(undefined);
+
+ var postTwo = new Decorated();
+ postTwo.path('title').should.not.equal(undefined);
+ should.strictEqual(undefined, postTwo.path('adhoc'));
+ db.close();
+ },
+
+ 'querying a document that had an on the fly schema should work': function () {
+ var db = start()
+ , Decorated = db.model('Decorated', collection);
+
+ var post = new Decorated({title: 'AD HOC'});
+ // Interpret adhoc as a Number
+ post.set('adhoc', '9', Number);
+ post.get('adhoc').valueOf().should.eql(9);
+ post.save( function (err) {
+ should.strictEqual(null, err);
+ Decorated.findById(post.id, function (err, found) {
+ db.close();
+ should.strictEqual(null, err);
+ found.get('adhoc').should.eql(9);
+ // Interpret adhoc as a String instead of a Number now
+ found.get('adhoc', String).should.eql('9');
+ found.get('adhoc').should.eql('9');
+ });
+ });
+ },
+
+ 'on the fly Embedded Array schemas should cast properly': function () {
+ var db = start()
+ , Decorated = db.model('Decorated', collection);
+
+ var post = new Decorated();
+ post.set('moderators', [{name: 'alex trebek'}], [new Schema({name: String})]);
+ post.get('moderators')[0].name.should.eql('alex trebek');
+ db.close();
+ },
+
+ 'on the fly Embedded Array schemas should get from a fresh queried document properly': function () {
+ var db = start()
+ , Decorated = db.model('Decorated', collection);
+
+ var post = new Decorated()
+ , ModeratorSchema = new Schema({name: String, ranking: Number});
+ post.set('moderators', [{name: 'alex trebek', ranking: '1'}], [ModeratorSchema]);
+ post.get('moderators')[0].name.should.eql('alex trebek');
+ post.save( function (err) {
+ should.strictEqual(null, err);
+ Decorated.findById(post.id, function (err, found) {
+ db.close();
+ should.strictEqual(null, err);
+ var rankingPreCast = found.get('moderators')[0].ranking;
+ rankingPreCast.should.eql(1);
+ should.strictEqual(undefined, rankingPreCast.increment);
+ var rankingPostCast = found.get('moderators', [ModeratorSchema])[0].ranking;
+ rankingPostCast.valueOf().should.equal(1);
+ rankingPostCast.increment.should.not.equal(undefined);
+
+ var NewModeratorSchema = new Schema({ name: String, ranking: String});
+ rankingPostCast = found.get('moderators', [NewModeratorSchema])[0].ranking;
+ rankingPostCast.should.equal('1');
+ });
+ });
+ }
+};

0 comments on commit 8e492e5

Please sign in to comment.