Skip to content

Commit

Permalink
Added tests and implementation for on-the-fly schemas.
Browse files Browse the repository at this point in the history
  • Loading branch information
bnoguchi committed May 5, 2011
1 parent 99e6a56 commit 8e492e5
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 42 deletions.
45 changes: 36 additions & 9 deletions lib/mongoose/document.js
Expand Up @@ -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')
Expand Down Expand Up @@ -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)
Expand All @@ -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) {
Expand All @@ -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;
Expand Down Expand Up @@ -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
*
Expand Down
3 changes: 2 additions & 1 deletion lib/mongoose/model.js
Expand Up @@ -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) {
Expand Down Expand Up @@ -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];
}
}
Expand Down
59 changes: 34 additions & 25 deletions lib/mongoose/schema.js
Expand Up @@ -4,7 +4,7 @@
*/

var EventEmitter = require('events').EventEmitter
, Types = require('./schema/index')
, Types
, VirtualType = require('./virtualtype')
, utils = require('./utils')
, NamedScope = require('./namedscope')
Expand Down Expand Up @@ -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 };

Expand All @@ -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);
};

/**
Expand Down Expand Up @@ -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;
24 changes: 18 additions & 6 deletions lib/mongoose/schematype.js
Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand Down
1 change: 0 additions & 1 deletion test/model.test.js
Expand Up @@ -3281,5 +3281,4 @@ module.exports = {
});
})
}

};
102 changes: 102 additions & 0 deletions test/schema.onthefly.test.js
@@ -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.