Using mongoose discriminators is probably the way to go here. They actually work with their own "type" ( default __t but can be overridden ) property within the stored documents which allows mongoose to actually apply a kind of "model" to each object with it's own attached schema.
As a brief example:
var async = require('async'),
util = require('util'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.connect('mongodb://localhost/things');
mongoose.set("debug",true);
function BaseSchema() {
Schema.apply(this,arguments);
this.add({
key: String,
created_at: { type: Date, default: Date.now }
});
}
util.inherits(BaseSchema,Schema);
var metaSchema = new BaseSchema();
var stringSchema = new BaseSchema({
value: String
});
var numberSchema = new BaseSchema({
value: Number
});
var dateSchema = new BaseSchema({
value: Date
});
var MetaModel = mongoose.model('MetaModel',metaSchema),
StringModel = MetaModel.discriminator('StringModel', stringSchema),
NumberModel = MetaModel.discriminator('NumberModel', numberSchema),
DateModel = MetaModel.discriminator('DateModel', dateSchema);
async.series(
[
function(callback) {
MetaModel.remove({},callback);
},
function(callback) {
async.each(
[
{ "model": "StringModel", "value": "Hello" },
{ "model": "NumberModel", "value": 12 },
{ "model": "DateModel", "value": new Date() }
],
function(item,callback) {
mongoose.model(item.model).create(item,callback)
},
callback
);
},
function(callback) {
MetaModel.find().exec(function(err,docs) {
console.log(docs);
callback(err);
});
},
function(callback) {
DateModel.findOne().exec(function(err,doc) {
console.log(doc);
callback(err);
});
}
],
function(err) {
if (err) throw err;
mongoose.disconnect();
}
)
So since these are basically "related" I am defining a "Base" schema with the common elements. Then of course there are separate schemas for each "type". The actual assignment to the core "model" happens in these lines:
var MetaModel = mongoose.model('MetaModel',metaSchema),
StringModel = MetaModel.discriminator('StringModel', stringSchema),
NumberModel = MetaModel.discriminator('NumberModel', numberSchema),
DateModel = MetaModel.discriminator('DateModel', dateSchema);
This means that MetaModel actually defines the collection and "default" schema assingment. The following lines use .discriminator() from that model in order to define the other "types" of documents that will be stored in the same collection.
With the debugging output on to show what is happening, the listing produces something like this:
Mongoose: metamodels.remove({}) {}
Mongoose: metamodels.insert({ value: 'Hello', __t: 'StringModel', created_at: new Date("Thu, 07 Apr 2016 00:24:08 GMT"), _id: ObjectId("5705a8a8443c0f74491bdec0"), __v: 0 })
Mongoose: metamodels.insert({ value: 12, __t: 'NumberModel', created_at: new Date("Thu, 07 Apr 2016 00:24:08 GMT"), _id: ObjectId("5705a8a8443c0f74491bdec1"), __v: 0 })
Mongoose: metamodels.insert({ value: new Date("Thu, 07 Apr 2016 00:24:08 GMT"), __t: 'DateModel', created_at: new Date("Thu, 07 Apr 2016 00:24:08 GMT"), _id: ObjectId("5705a8a8443c0f74491bdec2"), __v: 0 })
Mongoose: metamodels.find({}) { fields: undefined }
[ { created_at: Thu Apr 07 2016 10:24:08 GMT+1000 (AEST),
__t: 'StringModel',
__v: 0,
value: 'Hello',
_id: 5705a8a8443c0f74491bdec0 },
{ created_at: Thu Apr 07 2016 10:24:08 GMT+1000 (AEST),
__t: 'NumberModel',
__v: 0,
value: 12,
_id: 5705a8a8443c0f74491bdec1 },
{ created_at: Thu Apr 07 2016 10:24:08 GMT+1000 (AEST),
__t: 'DateModel',
__v: 0,
value: Thu Apr 07 2016 10:24:08 GMT+1000 (AEST),
_id: 5705a8a8443c0f74491bdec2 } ]
Mongoose: metamodels.findOne({ __t: 'DateModel' }) { fields: undefined }
{ created_at: Thu Apr 07 2016 10:24:08 GMT+1000 (AEST),
__t: 'DateModel',
__v: 0,
value: Thu Apr 07 2016 10:24:08 GMT+1000 (AEST),
_id: 5705a8a8443c0f74491bdec2 }
You can see that everything is being created in the metamodels collection assigned to the main model, however when referencing each "discriminator model" there is a __t field automatically created that includes the model name. This will be used later on reading the data back so mongoose knows which model and attached schema to apply when casting the objects.
Naturally since these all have their own schema, the standard validation rules apply. In addition any "instance methods" attached to the schema for each type also apply just as it would with any separate model.
Finally, that __t field is also applied with using one of the "discriminator models" for any other operation such as query or update. As shown in the last executed statement:
DateModel.findOne().exec(function(err,doc) {
console.log(doc);
callback(err);
});
And the actual call:
Mongoose: metamodels.findOne({ __t: 'DateModel' }) { fields: undefined }
That property value is included automatically to indicate the "type" and give a "virtual view" of the collection data just as if it only contained that particular type.
The real power actually lies in all objects being in the same collection and the ability of mongoose to assign the "class type" automatically on retrieval of data.
__t) which essentially makes each object bind to it's own pseudo model with it's own schema. Then you can have "strict" handling for each "type". That's a lot better than trying to hand roll any validation logic where you useMixed. - Neil Lunn