Skip to content

Commit

Permalink
feat(discriminator): basic PoC for document array discriminators
Browse files Browse the repository at this point in the history
  • Loading branch information
vkarpov15 committed Jan 18, 2017
1 parent 266076e commit 4af01f1
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 51 deletions.
21 changes: 20 additions & 1 deletion lib/model.js
Expand Up @@ -7,6 +7,7 @@ var Document = require('./document');
var DocumentNotFoundError = require('./error/notFound');
var DivergentArrayError = require('./error').DivergentArrayError;
var EventEmitter = require('events').EventEmitter;
var OverwriteModelError = require('./error').OverwriteModelError;
var PromiseProvider = require('./promise_provider');
var Query = require('./query');
var Schema = require('./schema');
Expand Down Expand Up @@ -823,7 +824,25 @@ Model.prototype.model = function model(name) {
*/

Model.discriminator = function(name, schema) {
return discriminator(this, name, schema);
discriminator(this, name, schema);
if (this.db.models[name]) {
throw new OverwriteModelError(name);
}

this.discriminators[name] = this.db.model(name, schema, this.collection.name);
var d = this.discriminators[name];
d.prototype.__proto__ = this.prototype;
Object.defineProperty(d, 'baseModelName', {
value: this.modelName,
configurable: true,
writable: false
});

// apply methods and statics
applyMethods(d, schema);
applyStatics(d, schema);

return d;
};

// Model (class) features
Expand Down
93 changes: 63 additions & 30 deletions lib/schema/documentarray.js
Expand Up @@ -11,6 +11,7 @@ var MongooseDocumentArray = require('../types/documentarray');
var SchemaType = require('../schematype');
var Subdocument = require('../types/embedded');
var applyHooks = require('../services/model/applyHooks');
var discriminator = require('../services/model/discriminator');
var util = require('util');

/**
Expand All @@ -24,6 +25,45 @@ var util = require('util');
*/

function DocumentArray(key, schema, options) {
var EmbeddedDocument = _createConstructor(schema, options);

ArrayType.call(this, key, EmbeddedDocument, options);

this.schema = schema;
this.$isMongooseDocumentArray = true;
var fn = this.defaultValue;

if (!('defaultValue' in this) || fn !== void 0) {
this.default(function() {
var arr = fn.call(this);
if (!Array.isArray(arr)) {
arr = [arr];
}
// Leave it up to `cast()` to convert this to a documentarray
return arr;
});
}
}

/**
* This schema type's name, to defend against minifiers that mangle
* function names.
*
* @api public
*/
DocumentArray.schemaName = 'DocumentArray';

/*!
* Inherits from ArrayType.
*/
DocumentArray.prototype = Object.create(ArrayType.prototype);
DocumentArray.prototype.constructor = DocumentArray;

/*!
* Ignore
*/

function _createConstructor(schema, options) {
// compile an embedded document for this schema
function EmbeddedDocument() {
Subdocument.apply(this, arguments);
Expand Down Expand Up @@ -53,37 +93,22 @@ function DocumentArray(key, schema, options) {

EmbeddedDocument.options = options;

ArrayType.call(this, key, EmbeddedDocument, options);

this.schema = schema;
this.$isMongooseDocumentArray = true;
var fn = this.defaultValue;

if (!('defaultValue' in this) || fn !== void 0) {
this.default(function() {
var arr = fn.call(this);
if (!Array.isArray(arr)) {
arr = [arr];
}
// Leave it up to `cast()` to convert this to a documentarray
return arr;
});
}
return EmbeddedDocument;
}

/**
* This schema type's name, to defend against minifiers that mangle
* function names.
*
* @api public
*/
DocumentArray.schemaName = 'DocumentArray';

/*!
* Inherits from ArrayType.
* Ignore
*/
DocumentArray.prototype = Object.create(ArrayType.prototype);
DocumentArray.prototype.constructor = DocumentArray;

DocumentArray.prototype.discriminator = function(name, schema) {
discriminator(this.casterConstructor, name, schema);

var EmbeddedDocument = _createConstructor(schema);

this.casterConstructor.discriminators[name] = EmbeddedDocument;

return this.casterConstructor.discriminators[name];
};

/**
* Performs local validations first, then validations on each embedded doc
Expand Down Expand Up @@ -236,9 +261,17 @@ DocumentArray.prototype.cast = function(value, doc, init, prev, options) {
if (!value[i]) {
continue;
}

var Constructor = this.casterConstructor;
if (Constructor.discriminators &&
typeof value[i][Constructor.schema.options.discriminatorKey] === 'string' &&
Constructor.discriminators[value[i][Constructor.schema.options.discriminatorKey]]) {
Constructor = Constructor.discriminators[value[i][Constructor.schema.options.discriminatorKey]];
}

// Check if the document has a different schema (re gh-3701)
if ((value[i] instanceof Subdocument) &&
value[i].schema !== this.casterConstructor.schema) {
value[i].schema !== Constructor.schema) {
value[i] = value[i].toObject({ transform: false, virtuals: false });
}
if (!(value[i] instanceof Subdocument) && value[i]) {
Expand All @@ -249,7 +282,7 @@ DocumentArray.prototype.cast = function(value, doc, init, prev, options) {
selected = true;
}

subdoc = new this.casterConstructor(null, value, true, selected, i);
subdoc = new Constructor(null, value, true, selected, i);
value[i] = subdoc.init(value[i]);
} else {
if (prev && (subdoc = prev.id(value[i]._id))) {
Expand All @@ -265,7 +298,7 @@ DocumentArray.prototype.cast = function(value, doc, init, prev, options) {
value[i] = subdoc;
} else {
try {
subdoc = new this.casterConstructor(value[i], value, undefined,
subdoc = new Constructor(value[i], value, undefined,
undefined, i);
// if set() is hooked it will have no return value
// see gh-746
Expand Down
20 changes: 0 additions & 20 deletions lib/services/model/discriminator.js
@@ -1,8 +1,5 @@
'use strict';

var OverwriteModelError = require('../../error').OverwriteModelError;
var applyMethods = require('./applyMethods');
var applyStatics = require('./applyStatics');
var utils = require('../../utils');

var CUSTOMIZABLE_DISCRIMINATOR_OPTIONS = {
Expand Down Expand Up @@ -100,21 +97,4 @@ module.exports = function discriminator(model, name, schema) {
if (model.discriminators[name]) {
throw new Error('Discriminator with name "' + name + '" already exists');
}
if (model.db.models[name]) {
throw new OverwriteModelError(name);
}

model.discriminators[name] = model.db.model(name, schema, model.collection.name);
model.discriminators[name].prototype.__proto__ = model.prototype;
Object.defineProperty(model.discriminators[name], 'baseModelName', {
value: model.modelName,
configurable: true,
writable: false
});

// apply methods and statics
applyMethods(model.discriminators[name], schema);
applyStatics(model.discriminators[name], schema);

return model.discriminators[name];
};
53 changes: 53 additions & 0 deletions test/model.discriminator.test.js
Expand Up @@ -378,6 +378,59 @@ describe('model', function() {
done();
});
});

it('embedded in document arrays (gh-2723)', function(done) {
var eventSchema = new Schema({ message: String },
{ discriminatorKey: 'kind', _id: false });

var batchSchema = new Schema({ events: [eventSchema] });
batchSchema.path('events').discriminator('Clicked', new Schema({
element: String
}, { _id: false }));
batchSchema.path('events').discriminator('Purchased', new Schema({
product: String
}, { _id: false }));

var MyModel = db.model('gh2723', batchSchema);
var doc = {
events: [
{ kind: 'Clicked', element: 'Test' },
{ kind: 'Purchased', product: 'Test2' }
]
};
MyModel.create(doc).
then(function(doc) {
assert.equal(doc.events.length, 2);
assert.equal(doc.events[0].element, 'Test');
assert.equal(doc.events[1].product, 'Test2');
var obj = doc.toObject({ virtuals: false });
delete obj._id;
assert.deepEqual(obj, {
__v: 0,
events: [
{ kind: 'Clicked', element: 'Test' },
{ kind: 'Purchased', product: 'Test2' }
]
});
done();
}).
then(function() {
return MyModel.findOne({
events: {
$elemMatch: {
kind: 'Clicked',
element: 'Test'
}
}
}, { 'events.$': 1 });
}).
then(function(doc) {
assert.ok(doc);
assert.equal(doc.events.length, 1);
assert.equal(doc.events[0].element, 'Test');
}).
catch(done);
});
});
});
});

0 comments on commit 4af01f1

Please sign in to comment.