Created
January 16, 2013 15:38
-
-
Save lukesh/4548045 to your computer and use it in GitHub Desktop.
Schema creation wrapper
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Schema, which wraps Mongoose's Schema for pretty and extendable models. | |
var _ = require('underscore') | |
, each = require('each') | |
, mongoose = require('mongoose') | |
, Schema = function(obj) { | |
this.schema = new mongoose.Schema(obj) | |
this.references = [] | |
} | |
// ### configure(obj) | |
// Accepts either a lambda that accepts a schema, or an object that contains | |
// Mongoose schema configuration. This can be used to extend the schema or to | |
// configure the schema directly. | |
Schema.prototype.configure = function(obj) { | |
if(typeof obj === 'function') { | |
obj(this.schema) | |
} else { | |
this.schema.add(obj) | |
} | |
return this | |
} | |
// ### plugin(path_to_plugin) | |
// Accepts a path to a Mongoose schema plugin and attaches the plugin to the | |
// schema. | |
Schema.prototype.plugin = function(plugin) { | |
this.schema.plugin(require(plugin)) | |
return this | |
} | |
// ### method(name, func) | |
// Appends a method ([func]) named [name] to the schema. This simply wraps the | |
// native mongoose.Schema.method method. | |
Schema.prototype.method = function(name, func) { | |
this.schema.method(name, func) | |
return this | |
} | |
// ### compile(name) | |
// This should be called last in the method chain, and assembles the Mongoose | |
// Schema and registers it as [name] as well as any sub-schemas as defined by | |
// the relational methods. | |
Schema.prototype.compile = function(name) { | |
var self = this | |
this.method('out', function(callback) { | |
var model = this | |
// It would be really nice if you could populate a model that's already | |
// instantiated. This is a big limitation in Mongoose that if resolved, | |
// would save most of this headache. | |
, response = this.toObject() | |
, sanitize = this.sanitize | |
// Run the sanitization function on the response if the model implements | |
// it. | |
if(sanitize) { | |
sanitize()(response) | |
} | |
// If the model doesn't have any embedded documents, just respond with the | |
// sanitized model. | |
if(self.references.length == 0) { | |
callback(response) | |
return | |
} | |
// Iterate through each reference, and return the lightweight | |
// representation of them. | |
each(self.references) | |
.on('item', function(ref, i, next) { | |
// Assinging the field whatever corresponding value exists in the model | |
// for now, which will transform later. It will either be an ObjectId or | |
// an object that contains an _id. | |
response[ref.field] = model[ref.field] | |
// If the value exists in the model, which it should, process it. | |
// Otherwise fail nicely by just returning whatever we have. | |
if(model[ref.field]) { | |
// We need to test if the field is an object or a collection. If it a | |
// collection, process each item in the collection. | |
if(model[ref.field].length !== undefined) { | |
each(model[ref.field]) | |
.on('item', function(item, i, next) { | |
// Get the model either by the generated schema name or by the | |
// explicit schema name. | |
mongoose.model(typeof ref.schema === 'string' ? ref.schema : ref.relationName) | |
// The item in the collection will either be an ObjectId or | |
// an object with an _id; use that to find the referenced | |
// object. | |
.findOne({ _id: item._id || item }) | |
.exec(function(err, obj) { | |
// Replace the item in the collection with the lightweight | |
// representation if it's available. | |
model[ref.field][i] = obj.lightweight ? obj.lightweight() : obj | |
next() | |
}) | |
}) | |
.on('error', function(err, errors) { | |
console.log('Error processing array-type reference in schema.js', err) | |
}) | |
.on('end', function() { | |
next() | |
}) | |
} else { | |
// Get the model either by the generated schema name or by the | |
// explicit schema name. | |
mongoose.model(typeof ref.schema === 'string' ? ref.schema : ref.relationName) | |
// The item will either be an ObjectId or an object with an _id; | |
// use that to find the referenced object. | |
.findOne({ _id: model[ref.field]._id || model[ref.field] }) | |
.exec(function(err, obj) { | |
// Replace the item with the lightweight representation if | |
// it's available. | |
response[ref.field] = obj.lightweight ? obj.lightweight() : obj | |
next() | |
}) | |
} | |
} else { | |
next() | |
} | |
}) | |
.on('error', function(err, errors) { | |
console.log('Error processing references in schema.js', err) | |
}) | |
.on('end', function() { | |
callback(response) | |
}) | |
}) | |
// Iterate through the references to and assemble the schemaObject. | |
_.each(this.references, function(e, i) { | |
// Generate a relation name based on the name of the parent (containing) | |
// object. This is useful if we need to register a dynamic schema. | |
e.relationName = e.relationName.split('{{parent}}').join(name) | |
var schemaObject = {} | |
, get = function(key) { | |
var response = { | |
_id: key | |
} | |
return key | |
} | |
, set = function(key, val) { | |
return key._id | |
} | |
// Assemble the schema object for each relation. | |
// It's either an object or a collection. This could be more elegant. | |
switch(e.relation) { | |
case 'one': | |
schemaObject[e.field] = {type: mongoose.Schema.ObjectId, ref: typeof e.schema === 'string' ? e.schema : e.relationName, get: get, set: set} | |
if(e.options) { | |
_.extend(schemaObject[e.field], e.options) | |
} | |
break | |
case 'many': | |
schemaObject[e.field] = [{type: mongoose.Schema.ObjectId, ref: typeof e.schema === 'string' ? e.schema : e.relationName, get: get, set: set}] | |
if(e.options) { | |
_.extend(schemaObject[e.field][0], e.options) | |
} | |
break | |
default: | |
} | |
// If it's a complex schema, recursively compile that. | |
if(typeof e.schema !== 'string') { | |
e.schema.compile(e.relationName) | |
} | |
// Add the schema object to the schema. | |
self.schema.add(schemaObject) | |
}) | |
// Finally, register the schema with the model name. | |
mongoose.model(name, this.schema) | |
return this | |
} | |
// ## Relational methods | |
// These are used to define and extend the schema's relationships to sub- | |
// schemas. The first return value is a "has" object that contains the | |
// following methods: | |
// one(schema): Accepts either a name (string) or a schema (schema) | |
// many(schema): Accepts either a name (string) or a schema (schema) | |
// ### requires(name) | |
// Returns a "has" object as a required schema property. | |
Schema.prototype.requires = function(name) { | |
return this.has(name, { required: true }) | |
} | |
// ### has(name, [options]) | |
// Returns a "has" object with no options by default. | |
Schema.prototype.has = function(name, options) { | |
var self = this | |
,reference = { | |
field: name | |
,options: options | |
// #### one(schema) | |
,one: function(schema) { | |
this.relation = 'one' | |
this.relationName = (function(str) { | |
return str.charAt(0).toUpperCase() + str.slice(1) | |
})(this.field) | |
this.schema = schema | |
return self | |
} | |
// #### many(schema) | |
,many: function(schema) { | |
this.relation = 'many' | |
this.relationName = (function(str) { | |
return '{{parent}}' + str | |
})((function(str) { | |
return str.slice(0, -1) | |
})((function(str) { | |
return str.charAt(0).toUpperCase() + str.slice(1) | |
})(this.field))) | |
this.schema = schema | |
return self | |
} | |
} | |
this.references.push(reference) | |
return reference | |
} | |
exports.new = function(obj) { | |
return new Schema(obj) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment