Skip to main content
Module

x/mongoose/test/model.discriminator.test.js

MongoDB object modeling designed to work in an asynchronous environment.
Go to Latest
File
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081
'use strict';
/** * Test dependencies. */
const start = require('./common');
const assert = require('assert');const clone = require('../lib/utils').clone;const random = require('./util').random;const util = require('util');
const mongoose = start.mongoose;const Schema = mongoose.Schema;
/** * Setup */const PersonSchema = new Schema({ name: { first: String, last: String }, gender: String}, { collection: 'model-discriminator-' + random() });PersonSchema.index({ name: 1 });PersonSchema.methods.getFullName = function() { return this.name.first + ' ' + this.name.last;};PersonSchema.methods.toJSonConfig = { include: ['prop1', 'prop2'], exclude: ['prop3', 'prop4']};PersonSchema.statics.findByGender = function() {};PersonSchema.virtual('name.full').get(function() { return this.name.first + ' ' + this.name.last;});PersonSchema.virtual('name.full').set(function(name) { const split = name.split(' '); this.name.first = split[0]; this.name.last = split[1];});PersonSchema.path('gender').validate(function(value) { return /[A-Z]/.test(value);}, 'Invalid name');PersonSchema.set('toObject', { getters: true, virtuals: true });PersonSchema.set('toJSON', { getters: true, virtuals: true });
const EmployeeSchema = new Schema({ department: String });EmployeeSchema.index({ department: 1 });EmployeeSchema.methods.getDepartment = function() { return this.department;};EmployeeSchema.statics.findByDepartment = function() {};EmployeeSchema.path('department').validate(function(value) { return /[a-zA-Z]/.test(value);}, 'Invalid name');const employeeSchemaPreSaveFn = function(next) { next();};EmployeeSchema.pre('save', employeeSchemaPreSaveFn);EmployeeSchema.set('toObject', { getters: true, virtuals: false });EmployeeSchema.set('toJSON', { getters: false, virtuals: true });
describe('model', function() { let db;
before(function() { db = start(); });
after(async function() { await db.close(); });
beforeEach(() => db.deleteModel(/.*/)); afterEach(() => require('./util').clearTestData(db)); afterEach(() => require('./util').stopRemainingOps(db));
describe('discriminator()', function() { let Person, Employee;
before(function() { db = start(); Person = db.model('Test', PersonSchema); Employee = Person.discriminator('Employee', EmployeeSchema); });
it('model defaults without discriminator', function() { const Model = db.model('Test1', new Schema()); assert.equal(Model.discriminators, undefined); });
it('is instance of root', function() { assert.equal(Employee.baseModelName, 'Test'); const employee = new Employee(); assert.ok(employee instanceof Person); assert.ok(employee instanceof Employee); });
it('can define static and instance methods', function() { function BossBaseSchema() { Schema.apply(this, arguments);
this.add({ name: String, createdAt: Date }); }
util.inherits(BossBaseSchema, Schema);
const PersonSchema = new BossBaseSchema(); const BossSchema = new BossBaseSchema({ department: String }); BossSchema.methods.myName = function() { return this.name; }; BossSchema.statics.currentPresident = function() { return 'obama'; }; const Person = db.model('Person', PersonSchema); const Boss = Person.discriminator('Boss', BossSchema);
const boss = new Boss({ name: 'Bernenke' }); assert.equal(boss.myName(), 'Bernenke'); assert.equal(boss.notInstanceMethod, undefined); assert.equal(Boss.currentPresident(), 'obama'); assert.equal(Boss.notStaticMethod, undefined); });
it('can define virtuals and methods using schema options (gh-12246)', function() { const baseSchema = new mongoose.Schema({ name: String }, { virtuals: { virtualA: { get: () => 'virtualA' } } });
const discriminatorSchema = new mongoose.Schema({ prop: String }, { virtuals: { virtualB: { get: () => 'virtualB' } } }); const BaseModel = db.model('Test', baseSchema); const DiscriminatorModel = BaseModel.discriminator('Test1', discriminatorSchema);
const doc = new DiscriminatorModel(); assert.equal(doc.virtualA, 'virtualA'); assert.equal(doc.virtualB, 'virtualB'); });
it('sets schema root discriminator mapping', function(done) { assert.deepEqual(Person.schema.discriminatorMapping, { key: '__t', value: null, isRoot: true }); done(); });
it('sets schema discriminator type mapping', function(done) { assert.deepEqual(Employee.schema.discriminatorMapping, { key: '__t', value: 'Employee', isRoot: false }); done(); });
it('adds discriminatorKey to schema with default as name', function() { const type = Employee.schema.paths.__t; assert.equal(type.options.type, String); assert.equal(type.options.default, 'Employee'); });
it('adds discriminator to Model.discriminators object', function() { assert.equal(Object.keys(Person.discriminators).length, 1); assert.equal(Person.discriminators['Employee'], Employee); const newName = 'model-discriminator-' + random(); const NewDiscriminatorType = Person.discriminator(newName, new Schema()); assert.equal(Object.keys(Person.discriminators).length, 2); assert.equal(Person.discriminators[newName], NewDiscriminatorType); });
it('throws error on invalid schema', function() { assert.throws( function() { Person.discriminator('Foo'); }, /You must pass a valid discriminator Schema/ ); });
it('throws error when attempting to nest discriminators', function() { assert.throws( function() { Employee.discriminator('model-discriminator-foo', new Schema()); }, /Discriminator "model-discriminator-foo" can only be a discriminator of the root model/ ); });
it('throws error when discriminator has mapped discriminator key in schema', function() { assert.throws( function() { Person.discriminator('model-discriminator-foo', new Schema({ __t: String })); }, /Discriminator "model-discriminator-foo" cannot have field with name "__t"/ ); });
it('throws error when discriminator has mapped discriminator key in schema with discriminatorKey option set', function() { assert.throws( function() { const Foo = db.model('Test1', new Schema({}, { discriminatorKey: '_type' })); Foo.discriminator('Bar', new Schema({ _type: String })); }, /Discriminator "Bar" cannot have field with name "_type"/ ); });
it('throws error when discriminator with taken name is added', function() { const Foo = db.model('Test1', new Schema({})); Foo.discriminator('Token', new Schema()); assert.throws( function() { Foo.discriminator('Token', new Schema()); }, /Discriminator with name "Token" already exists/ ); });
it('throws error if model name is taken (gh-4148)', function() { const Foo = db.model('Test1', new Schema({})); db.model('Test', new Schema({})); assert.throws( function() { Foo.discriminator('Test', new Schema()); }, /Cannot overwrite `Test`/); });
it('works with nested schemas (gh-2821)', function(done) { const MinionSchema = function() { mongoose.Schema.apply(this, arguments);
this.add({ name: String }); }; util.inherits(MinionSchema, mongoose.Schema);
const BaseSchema = function() { mongoose.Schema.apply(this, arguments);
this.add({ name: String, created_at: Date, minions: [new MinionSchema()] }); }; util.inherits(BaseSchema, mongoose.Schema);
const PersonSchema = new BaseSchema(); const BossSchema = new BaseSchema({ department: String }, { id: false });
// Should not throw const Person = db.model('Test1', PersonSchema); Person.discriminator('Boss', BossSchema); done(); });
describe('options', function() { it('allows toObject to be overridden', function(done) { assert.notDeepEqual(Employee.schema.get('toObject'), Person.schema.get('toObject')); assert.deepEqual(Employee.schema.get('toObject'), { getters: true, virtuals: false }); done(); });
it('allows toJSON to be overridden', function(done) { assert.notDeepEqual(Employee.schema.get('toJSON'), Person.schema.get('toJSON')); assert.deepEqual(Employee.schema.get('toJSON'), { getters: false, virtuals: true }); done(); });
it('is not customizable', function() { const CustomizedSchema = new Schema({}, { capped: true });
assert.throws(function() { Person.discriminator('model-discriminator-custom', CustomizedSchema); }, /Can't customize discriminator option capped/); }); });
describe('root schema inheritance', function() { it('inherits field mappings', function() { assert.strictEqual(Employee.schema.path('name'), Person.schema.path('name')); assert.strictEqual(Employee.schema.path('gender'), Person.schema.path('gender')); assert.equal(Person.schema.paths.department, undefined); });
it('inherits validators', function() { assert.strictEqual(Employee.schema.path('gender').validators, PersonSchema.path('gender').validators); assert.deepEqual(Employee.schema.path('department').validators, EmployeeSchema.path('department').validators); });
it('does not inherit and override fields that exist', function() { const FemaleSchema = new Schema({ gender: { type: String, default: 'F' } }), Female = Person.discriminator('model-discriminator-female', FemaleSchema);
const gender = Female.schema.paths.gender;
assert.notStrictEqual(gender, Person.schema.paths.gender); assert.equal(gender.instance, 'String'); assert.equal(gender.options.default, 'F'); });
it('inherits methods', function() { const employee = new Employee(); assert.strictEqual(employee.getFullName, PersonSchema.methods.getFullName); assert.strictEqual(employee.getDepartment, EmployeeSchema.methods.getDepartment); assert.equal((new Person()).getDepartment, undefined); });
it('inherits statics', function() { assert.strictEqual(Employee.findByGender, PersonSchema.statics.findByGender); assert.strictEqual(Employee.findByDepartment, EmployeeSchema.statics.findByDepartment); assert.equal(Person.findByDepartment, undefined); });
it('inherits virtual (g.s)etters', function() { const employee = new Employee(); employee.name.full = 'John Doe'; assert.equal(employee.name.full, 'John Doe'); });
it('does not inherit indexes', function() { assert.deepEqual(Person.schema.indexes(), [[{ name: 1 }, { background: true }]]); assert.deepEqual( Employee.schema.indexes(), [[{ department: 1 }, { background: true, partialFilterExpression: { __t: 'Employee' } }]] ); });
it('gets options overridden by root options except toJSON and toObject', function() { const personOptions = clone(Person.schema.options), employeeOptions = clone(Employee.schema.options);
delete personOptions.toJSON; delete personOptions.toObject; delete employeeOptions.toJSON; delete employeeOptions.toObject;
assert.deepEqual(personOptions, employeeOptions); });
it('does not allow setting discriminator key (gh-2041)', function(done) { const doc = new Employee({ __t: 'fake' }); assert.equal(doc.__t, 'Employee'); doc.save(function(error) { assert.ok(error); assert.equal(error.errors['__t'].reason.message, 'Can\'t set discriminator key "__t"'); done(); }); });
it('deduplicates hooks (gh-2945)', function() { let called = 0; function middleware(next) { ++called; next(); }
function ActivityBaseSchema() { mongoose.Schema.apply(this, arguments); this.options.discriminatorKey = 'type'; this.add({ name: String }); this.pre('validate', middleware); } util.inherits(ActivityBaseSchema, mongoose.Schema);
const parentSchema = new ActivityBaseSchema();
const model = db.model('Test1', parentSchema);
const commentSchema = new ActivityBaseSchema({ text: { type: String, required: true } });
const D = model.discriminator('D', commentSchema);
return new D({ text: 'test' }).validate(). then(() => { assert.equal(called, 1); }); });
it('with typeKey (gh-4339)', function() { const options = { typeKey: '$type', discriminatorKey: '_t' }; const schema = new Schema({ test: { $type: String } }, options); const Model = db.model('Test', schema); Model.discriminator('D', new Schema({ test2: String }, { typeKey: '$type' })); });
describe('applyPluginsToDiscriminators', function() { let m;
beforeEach(function() { m = new mongoose.Mongoose(); m.set('applyPluginsToDiscriminators', true); });
it('works (gh-4965)', function() { const schema = new m.Schema({ test: String }); let called = 0; m.plugin(function() { ++called; }); const Model = m.model('Test', schema); const childSchema = new m.Schema({ test2: String }); Model.discriminator('D', childSchema); assert.equal(called, 2); });
it('works with customized options (gh-7458)', function() { m.plugin((schema) => { schema.options.versionKey = false; schema.options.minimize = false; });
const schema = new m.Schema({ type: { type: String }, something: { type: String } }, { discriminatorKey: 'type' }); const Model = m.model('Test', schema);
const subSchema = new m.Schema({ somethingElse: { type: String } });
// Should not throw Model.discriminator('TestSub', subSchema); }); });
it('embedded discriminators with array defaults (gh-7687)', function() { const abstractSchema = new Schema({}, { discriminatorKey: 'kind', _id: false }); const concreteSchema = new Schema({ foo: { type: Number } }); const defaultValue = [{ kind: 'concrete', foo: 42 }];
const schema = new Schema({ items: { type: [abstractSchema], default: defaultValue } });
schema.path('items').discriminator('concrete', concreteSchema);
const Thing = db.model('Test', schema); const doc = new Thing();
assert.equal(doc.items[0].foo, 42); assert.equal(doc.items[0].constructor.name, 'concrete');
return Promise.resolve(); });
it('embedded discriminators with create() (gh-5001)', function() { const eventSchema = new Schema({ message: String }, { discriminatorKey: 'kind', _id: false }); const batchSchema = new Schema({ events: [eventSchema] }); const docArray = batchSchema.path('events');
docArray.discriminator('Clicked', new Schema({ element: { type: String, required: true } }, { _id: false }));
const Purchased = docArray.discriminator('Purchased', new Schema({ product: { type: String, required: true } }, { _id: false }));
const Batch = db.model('Test', batchSchema);
const batch = { events: [ { kind: 'Clicked', element: '#hero' } ] };
return Batch.create(batch). then(function(doc) { assert.equal(doc.events.length, 1); const newDoc = doc.events.create({ kind: 'Purchased', product: 'action-figure-1' }); assert.equal(newDoc.kind, 'Purchased'); assert.equal(newDoc.product, 'action-figure-1'); assert.ok(newDoc instanceof Purchased);
doc.events.push(newDoc); assert.equal(doc.events.length, 2); assert.equal(doc.events[1].kind, 'Purchased'); assert.equal(doc.events[1].product, 'action-figure-1'); assert.ok(newDoc instanceof Purchased); assert.ok(newDoc === doc.events[1]); }); });
it('embedded discriminator with numeric type (gh-7808)', async function() { const typesSchema = Schema({ type: { type: Number } }, { discriminatorKey: 'type', _id: false });
const mainSchema = Schema({ types: [typesSchema] });
mainSchema.path('types').discriminator(1, Schema({ foo: { type: String, default: 'bar' } })); mainSchema.path('types').discriminator(2, Schema({ hello: { type: String, default: 'world' } }));
const Model = db.model('Test1', mainSchema);
await Model.create({ types: [{ type: 1 }, { type: 2 }] }); const fromDb = await Model.collection.findOne(); assert.equal(fromDb.types.length, 2); assert.equal(fromDb.types[0].foo, 'bar'); assert.equal(fromDb.types[1].hello, 'world'); });
it('supports clone() (gh-4983)', function(done) { const childSchema = new Schema({ name: String }); let childCalls = 0; let childValidateCalls = 0; const preValidate = function preValidate(next) { ++childValidateCalls; next(); }; childSchema.pre('validate', preValidate); childSchema.pre('save', function(next) { ++childCalls; next(); });
const personSchema = new Schema({ name: String }, { discriminatorKey: 'kind' });
const parentSchema = new Schema({ children: [childSchema], heir: childSchema }); let parentCalls = 0; parentSchema.pre('save', function(next) { ++parentCalls; next(); });
const Person = db.model('Person', personSchema); const Parent = Person.discriminator('Parent', parentSchema.clone());
const obj = { name: 'Ned Stark', heir: { name: 'Robb Stark' }, children: [{ name: 'Jon Snow' }] }; const doc = new Parent(obj);
doc.save(function(error, doc) { assert.ifError(error); assert.equal(doc.name, 'Ned Stark'); assert.equal(doc.heir.name, 'Robb Stark'); assert.equal(doc.children.length, 1); assert.equal(doc.children[0].name, 'Jon Snow'); assert.equal(childValidateCalls, 2); assert.equal(childCalls, 2); assert.equal(parentCalls, 1); done(); }); });
it('clone() allows reusing schemas (gh-5098)', function() { const personSchema = new Schema({ name: String }, { discriminatorKey: 'kind' });
const parentSchema = new Schema({ child: String });
const Person = db.model('Person', personSchema); Person.discriminator('Parent', parentSchema.clone()); // Should not throw Person.discriminator('Parent2', parentSchema.clone()); });
it('clone() allows reusing with different models (gh-5721)', function(done) { const schema = new mongoose.Schema({ name: String });
const schemaExt = new mongoose.Schema({ nameExt: String });
const ModelA = db.model('Test1', schema); ModelA.discriminator('D1', schemaExt);
ModelA.findOneAndUpdate({}, { $set: { name: 'test' } }, function(error) { assert.ifError(error);
const ModelB = db.model('Test2', schema.clone()); ModelB.discriminator('D2', schemaExt.clone());
done(); }); });
it('incorrect discriminator key throws readable error with create (gh-6434)', async function() { const settingSchema = new Schema({ name: String }, { discriminatorKey: 'kind' });
const defaultAdvisorSchema = new Schema({ _advisor: String });
const Setting = db.model('Test', settingSchema); Setting.discriminator('DefaultAdvisor', defaultAdvisorSchema);
let threw = false; try { await Setting.create({ kind: 'defaultAdvisor', name: 'xyz' }); } catch (error) { threw = true; assert.equal(error.name, 'MongooseError'); assert.equal(error.message, 'Discriminator "defaultAdvisor" not ' + 'found for model "Test"'); } assert.ok(threw); });
it('copies query hooks (gh-5147)', function(done) { const options = { discriminatorKey: 'kind' };
const eventSchema = new mongoose.Schema({ time: Date }, options); let eventSchemaCalls = 0; eventSchema.pre('findOneAndUpdate', function() { ++eventSchemaCalls; });
const Event = db.model('Test', eventSchema);
const clickedEventSchema = new mongoose.Schema({ url: String }, options); let clickedEventSchemaCalls = 0; clickedEventSchema.pre('findOneAndUpdate', function() { ++clickedEventSchemaCalls; }); const ClickedLinkEvent = Event.discriminator('ClickedLink', clickedEventSchema);
ClickedLinkEvent.findOneAndUpdate({}, { time: new Date() }, {}). exec(function(error) { assert.ifError(error); assert.equal(eventSchemaCalls, 1); assert.equal(clickedEventSchemaCalls, 1); done(); }); });
it('reusing schema for discriminators (gh-5684)', function() { const ParentSchema = new Schema({}); const ChildSchema = new Schema({ name: String });
const FirstContainerSchema = new Schema({ stuff: [ParentSchema] });
FirstContainerSchema.path('stuff').discriminator('Child', ChildSchema);
const SecondContainerSchema = new Schema({ things: [ParentSchema] });
SecondContainerSchema.path('things').discriminator('Child', ChildSchema);
const M1 = db.model('Test1', FirstContainerSchema); const M2 = db.model('Test2', SecondContainerSchema);
const doc1 = new M1({ stuff: [{ __t: 'Child', name: 'test' }] }); const doc2 = new M2({ things: [{ __t: 'Child', name: 'test' }] });
assert.equal(doc1.stuff.length, 1); assert.equal(doc1.stuff[0].name, 'test'); assert.equal(doc2.things.length, 1); assert.equal(doc2.things[0].name, 'test'); });
it('overwrites nested paths in parent schema (gh-6076)', function() { const schema = mongoose.Schema({ account: { type: Object } });
const Model = db.model('Test1', schema);
const discSchema = mongoose.Schema({ account: { user: { ref: 'Foo', required: true, type: mongoose.Schema.Types.ObjectId } } });
const Disc = Model.discriminator('D', discSchema);
const d1 = new Disc({ account: { user: 'AAAAAAAAAAAAAAAAAAAAAAAA' }, info: 'AAAAAAAAAAAAAAAAAAAAAAAA' });
assert.ifError(d1.validateSync()); });
it('nested discriminator key with projecting in parent (gh-5775)', function(done) { const itemSchema = new Schema({ type: { type: String }, active: { type: Boolean, default: true } }, { discriminatorKey: 'type' });
const collectionSchema = new Schema({ items: [itemSchema] });
const s = new Schema({ count: Number }); collectionSchema.path('items').discriminator('type1', s);
const MyModel = db.model('Test', collectionSchema); const doc = { items: [{ type: 'type1', active: false, count: 3 }] }; MyModel.create(doc, function(error) { assert.ifError(error); MyModel.findOne({}).select('items').exec(function(error, doc) { assert.ifError(error); assert.equal(doc.items.length, 1); assert.equal(doc.items[0].type, 'type1'); assert.strictEqual(doc.items[0].active, false); assert.strictEqual(doc.items[0].count, 3); done(); }); }); });
it('with $meta projection (gh-5859)', function() { const eventSchema = new Schema({ eventField: String }, { id: false }); eventSchema.index({ eventField: 'text' }); const Event = db.model('Test', eventSchema);
const trackSchema = new Schema({ trackField: String }); const Track = Event.discriminator('Track', trackSchema);
const trackedItem = new Track({ trackField: 'track', eventField: 'event' });
return trackedItem.save(). then(() => Event.init()). then(function() { return Event.find({ $text: { $search: 'event' } }). select({ score: { $meta: 'textScore' } }); }). then(function(docs) { assert.equal(docs.length, 1); assert.equal(docs[0].trackField, 'track'); }). then(function() { return Track.find({ $text: { $search: 'event' } }). select({ score: { $meta: 'textScore' } }); }). then(function(docs) { assert.equal(docs.length, 1); assert.equal(docs[0].trackField, 'track'); assert.equal(docs[0].eventField, 'event'); }); });
it('embedded discriminators with $push (gh-5009)', function(done) { const eventSchema = new Schema({ message: String }, { discriminatorKey: 'kind', _id: false }); const batchSchema = new Schema({ events: [eventSchema] }); const docArray = batchSchema.path('events');
docArray.discriminator('Clicked', new Schema({ element: { type: String, required: true } }, { _id: false }));
docArray.discriminator('Purchased', new Schema({ product: { type: String, required: true } }, { _id: false }));
const Batch = db.model('Test', batchSchema);
const batch = { events: [ { kind: 'Clicked', element: '#hero' } ] };
Batch.create(batch). then(function(doc) { assert.equal(doc.events.length, 1); return Batch.updateOne({ _id: doc._id }, { $push: { events: { kind: 'Clicked', element: '#button' } } }).then(function() { return doc; }); }). then(function(doc) { return Batch.findOne({ _id: doc._id }); }). then(function(doc) { assert.equal(doc.events.length, 2); assert.equal(doc.events[1].element, '#button'); assert.equal(doc.events[1].kind, 'Clicked'); done(); }). catch(done); });
it('embedded discriminators with $push + $each (gh-5070)', function(done) { const eventSchema = new Schema({ message: String }, { discriminatorKey: 'kind', _id: false }); const batchSchema = new Schema({ events: [eventSchema] }); const docArray = batchSchema.path('events');
docArray.discriminator('Clicked', new Schema({ element: { type: String, required: true } }, { _id: false }));
docArray.discriminator('Purchased', new Schema({ product: { type: String, required: true } }, { _id: false }));
const Batch = db.model('Test1', batchSchema);
const batch = { events: [ { kind: 'Clicked', element: '#hero' } ] };
Batch.create(batch). then(function(doc) { assert.equal(doc.events.length, 1); return Batch.updateOne({ _id: doc._id }, { $push: { events: { $each: [{ kind: 'Clicked', element: '#button' }] } } }).then(function() { return doc; }); }). then(function(doc) { return Batch.findOne({ _id: doc._id }); }). then(function(doc) { assert.equal(doc.events.length, 2); assert.equal(doc.events[1].element, '#button'); assert.equal(doc.events[1].kind, 'Clicked'); done(); }). catch(done); });
it('embedded discriminators with $set (gh-5130)', function(done) { const eventSchema = new Schema({ message: String }, { discriminatorKey: 'kind' }); const batchSchema = new Schema({ events: [eventSchema] }); const docArray = batchSchema.path('events');
docArray.discriminator('Clicked', new Schema({ element: { type: String, required: true } }));
docArray.discriminator('Purchased', new Schema({ product: { type: String, required: true } }));
const Batch = db.model('Test1', batchSchema);
const batch = { events: [ { kind: 'Clicked', element: '#hero' } ] };
Batch.create(batch). then(function(doc) { assert.equal(doc.events.length, 1); return Batch.updateOne({ _id: doc._id, 'events._id': doc.events[0]._id }, { $set: { 'events.$': { message: 'updated', kind: 'Clicked', element: '#hero2' } } }).then(function() { return doc; }); }). then(function(doc) { return Batch.findOne({ _id: doc._id }); }). then(function(doc) { assert.equal(doc.events.length, 1); assert.equal(doc.events[0].message, 'updated'); assert.equal(doc.events[0].element, '#hero2'); // <-- test failed assert.equal(doc.events[0].kind, 'Clicked'); // <-- test failed done(); }). catch(done); });
it('embedded in document arrays (gh-2723)', function(done) { const eventSchema = new Schema({ message: String }, { discriminatorKey: 'kind', _id: false });
const 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 }));
const MyModel = db.model('Test1', batchSchema); const 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'); const obj = doc.toObject({ virtuals: false }); delete obj._id; assert.deepEqual(obj, { __v: 0, events: [ { kind: 'Clicked', element: 'Test' }, { kind: 'Purchased', product: 'Test2' } ] }); }). 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'); done(); }). catch(done); }); });
it('embedded with single nested subdocs (gh-5244)', function(done) { const eventSchema = new Schema({ message: String }, { discriminatorKey: 'kind', _id: false });
const trackSchema = new Schema({ event: eventSchema }); trackSchema.path('event').discriminator('Clicked', new Schema({ element: String }, { _id: false })); trackSchema.path('event').discriminator('Purchased', new Schema({ product: String }, { _id: false }));
const MyModel = db.model('Test1', trackSchema); const doc1 = { event: { kind: 'Clicked', element: 'Amazon Link' } }; const doc2 = { event: { kind: 'Purchased', product: 'Professional AngularJS' } }; MyModel.create([doc1, doc2]). then(function(docs) { const doc1 = docs[0]; const doc2 = docs[1];
assert.equal(doc1.event.kind, 'Clicked'); assert.equal(doc1.event.element, 'Amazon Link'); assert.ok(!doc1.event.product);
assert.equal(doc2.event.kind, 'Purchased'); assert.equal(doc2.event.product, 'Professional AngularJS'); assert.ok(!doc2.event.element); done(); }). catch(done); });
it('embedded with single nested subdocs and tied value (gh-8164) (gh-9108)', function() { const eventSchema = new Schema({ message: String }, { discriminatorKey: 'kind', _id: false });
const trackSchema = new Schema({ event: eventSchema }); trackSchema.path('event').discriminator('Clicked', new Schema({ element: String }, { _id: false }), 'click'); trackSchema.path('event').discriminator('Purchased', new Schema({ product: String }, { _id: false }), 'purchase');
const MyModel = db.model('Test1', trackSchema); let doc1 = { event: { kind: 'click', element: 'Amazon Link' } }; let doc2 = { event: { kind: 'purchase', product: 'Professional AngularJS' } }; return MyModel.create([doc1, doc2]). then(function(docs) { doc1 = docs[0]; doc2 = docs[1];
assert.equal(doc1.event.kind, 'click'); assert.equal(doc1.event.element, 'Amazon Link'); assert.ok(!doc1.event.product);
assert.equal(doc2.event.kind, 'purchase'); assert.equal(doc2.event.product, 'Professional AngularJS'); assert.ok(!doc2.event.element);
return MyModel.updateOne({ 'event.kind': 'click' }, { 'event.element': 'Pluralsight Link' }); }). then(() => MyModel.findById(doc1._id)). then(doc => { assert.equal(doc.event.element, 'Pluralsight Link'); }); });
it('supports ObjectId as tied value (gh-10130)', async function() { const eventSchema = new Schema({ message: String, kind: 'ObjectId' }, { discriminatorKey: 'kind' });
const Event = db.model('Event', eventSchema); const clickedId = new mongoose.Types.ObjectId(); const purchasedId = new mongoose.Types.ObjectId(); Event.discriminator('Clicked', new Schema({ element: String }), clickedId); Event.discriminator('Purchased', new Schema({ product: String }), purchasedId);
await Event.create([ { message: 'test', element: '#buy', kind: clickedId }, { message: 'test2', product: 'Turbo Man', kind: purchasedId } ]);
const docs = await Event.find().sort({ message: 1 }); assert.equal(docs.length, 2); assert.equal(docs[0].kind.toHexString(), clickedId.toHexString()); assert.equal(docs[0].element, '#buy'); assert.equal(docs[1].kind.toHexString(), purchasedId.toHexString()); assert.equal(docs[1].product, 'Turbo Man'); });
it('Embedded discriminators in nested doc arrays (gh-6202)', function() { const eventSchema = new Schema({ message: String }, { discriminatorKey: 'kind', _id: false });
const batchSchema = new Schema({ events: [[eventSchema]] }); const docArray = batchSchema.path('events');
const clickedSchema = new Schema({ element: { type: String, required: true } }, { _id: false }); docArray.discriminator('Clicked', clickedSchema);
const M = db.model('Test1', batchSchema);
return M.create({ events: [[{ kind: 'Clicked', element: 'foo' }]] }). then(() => M.findOne()). then(doc => { assert.deepEqual(doc.toObject().events[0][0], { kind: 'Clicked', element: 'foo' }); }); });
it('throws an error if calling discriminator on non-doc array (gh-6202)', function() { const batchSchema = new Schema({ events: [[Number]] }); const arr = batchSchema.path('events');
const clickedSchema = new Schema({ element: { type: String, required: true } }, { _id: false });
let threw = false; try { arr.discriminator('Clicked', clickedSchema); } catch (error) { threw = true; assert.ok(error.message.indexOf('embedded discriminator') !== -1, error.message); } assert.ok(threw); });
it('supports using a schema that was used for another discriminator (gh-7200)', function() { const schema = new Schema({ name: String, names: [{ name: String }] });
const conA = mongoose.createConnection(start.uri);
const schemaExt = new Schema({ nameExt: String });
const modelA = conA.model('Test', schema); modelA.discriminator('AExt', schemaExt);
const conB = mongoose.createConnection(start.uri);
const modelB = conB.model('Test1', schema); modelB.discriminator('AExt', schemaExt);
});
describe('embedded discriminators + hooks (gh-5706)', function() { const counters = { eventPreSave: 0, eventPostSave: 0, purchasePreSave: 0, purchasePostSave: 0, eventPreValidate: 0, eventPostValidate: 0, purchasePreValidate: 0, purchasePostValidate: 0 }; const eventSchema = new Schema( { message: String }, { discriminatorKey: 'kind', _id: false } ); eventSchema.pre('validate', function(next) { counters.eventPreValidate++; next(); });
eventSchema.post('validate', function() { counters.eventPostValidate++; });
eventSchema.pre('save', function(next) { counters.eventPreSave++; next(); });
eventSchema.post('save', function() { counters.eventPostSave++; });
const purchasedSchema = new Schema({ product: String }, { _id: false });
purchasedSchema.pre('validate', function(next) { counters.purchasePreValidate++; next(); });
purchasedSchema.post('validate', function() { counters.purchasePostValidate++; });
purchasedSchema.pre('save', function(next) { counters.purchasePreSave++; next(); });
purchasedSchema.post('save', function() { counters.purchasePostSave++; });
beforeEach(function() { Object.keys(counters).forEach(function(i) { counters[i] = 0; }); });
it('should call the hooks on the embedded document defined by both the parent and discriminated schemas', function(done) { const trackSchema = new Schema({ event: eventSchema });
const embeddedEventSchema = trackSchema.path('event'); embeddedEventSchema.discriminator('Purchased', purchasedSchema.clone());
const TrackModel = db.model('Track', trackSchema); const doc = new TrackModel({ event: { message: 'Test', kind: 'Purchased' } }); doc.save(function(err) { assert.ok(!err); assert.equal(doc.event.message, 'Test'); assert.equal(doc.event.kind, 'Purchased'); Object.keys(counters).forEach(function(i) { assert.equal(counters[i], 1, 'Counter ' + i + ' incorrect'); }); done(); }); });
it('should call the hooks on the embedded document in an embedded array defined by both the parent and discriminated schemas', function(done) { const trackSchema = new Schema({ events: [eventSchema] });
const embeddedEventSchema = trackSchema.path('events'); embeddedEventSchema.discriminator('Purchased', purchasedSchema.clone());
const TrackModel = db.model('Track', trackSchema); const doc = new TrackModel({ events: [ { message: 'Test', kind: 'Purchased' }, { message: 'TestAgain', kind: 'Purchased' } ] }); doc.save(function(err) { assert.ok(!err); assert.equal(doc.events[0].kind, 'Purchased'); assert.equal(doc.events[0].message, 'Test'); assert.equal(doc.events[1].kind, 'Purchased'); assert.equal(doc.events[1].message, 'TestAgain'); Object.keys(counters).forEach(function(i) { assert.equal(counters[i], 2); }); done(); }); }); });
it('should copy plugins', function() { const plugin = () => { };
const schema = new Schema({ value: String }, { autoIndex: false, autoCreate: false }); schema.plugin(plugin); mongoose.deleteModel(/Test/); const model = mongoose.model('Test', schema);
const discriminator = model.discriminator('Desc', new Schema({ anotherValue: String }));
const copiedPlugin = discriminator.schema.plugins.find(p => p.fn === plugin); assert.ok(!!copiedPlugin);
mongoose.deleteModel(/Model/); }); });
describe('bug fixes', function() { it('discriminators with classes modifies class in place (gh-5175)', function(done) { class Vehicle extends mongoose.Model { } const V = mongoose.model(Vehicle, new mongoose.Schema()); assert.ok(V === Vehicle); class Car extends Vehicle { } const C = Vehicle.discriminator(Car, new mongoose.Schema()); assert.ok(C === Car); done(); });
it('allows overwriting base class methods (gh-5227)', function(done) { class BaseModel extends mongoose.Model { getString() { return 'parent'; } }
class Test extends BaseModel { getString() { return 'child'; } }
mongoose.deleteModel(/Test/); const UserModel = mongoose.model(Test, new mongoose.Schema({}));
const u = new UserModel({});
assert.equal(u.getString(), 'child');
done(); });
it('supports adding properties (gh-5104) (gh-5635)', function(done) { class Shape extends mongoose.Model { } class Circle extends Shape { }
const ShapeModel = mongoose.model(Shape, new mongoose.Schema({ color: String }));
ShapeModel.discriminator(Circle, new mongoose.Schema({ radius: Number }));
const circle = new Circle({ color: 'blue', radius: 3 }); assert.equal(circle.color, 'blue'); assert.equal(circle.radius, 3);
done(); });
it('with subclassing (gh-7547)', function() { const options = { discriminatorKey: 'kind' };
const eventSchema = new mongoose.Schema({ time: Date }, options); mongoose.deleteModel(/Test/); const eventModelUser1 = mongoose.model('Test', eventSchema, 'tests'); const eventModelUser2 = mongoose.model('Test', eventSchema, 'test1');
const discSchema = new mongoose.Schema({ url: String }, options); const clickEventUser1 = eventModelUser1. discriminator('ClickedEvent', discSchema); const clickEventUser2 = eventModelUser2.discriminators['ClickedEvent'];
assert.equal(clickEventUser1.collection.name, 'tests'); assert.equal(clickEventUser2.collection.name, 'test1'); });
it('uses correct discriminator when using `new BaseModel` (gh-7586)', function() { const options = { discriminatorKey: 'kind' };
const BaseModel = mongoose.model('Parent', Schema({ name: String }, options)); const ChildModel = BaseModel.discriminator('Child', Schema({ test: String }, options));
const doc = new BaseModel({ kind: 'Child', name: 'a', test: 'b' }); assert.ok(doc instanceof ChildModel); assert.equal(doc.test, 'b'); });
it('uses correct discriminator when using `new BaseModel` with value (gh-7851)', function() { const options = { discriminatorKey: 'kind' };
const BaseModel = db.model('Parent', Schema({ name: String }, options)); const ChildModel = BaseModel.discriminator('Child', Schema({ test: String }, options), 'child');
const doc = new BaseModel({ kind: 'child', name: 'a', test: 'b' }); assert.ok(doc instanceof ChildModel); assert.equal(doc.test, 'b'); });
it('allows setting custom discriminator key in schema (gh-7807)', function() { const eventSchema = Schema({ title: String, kind: { type: String, required: true } }, { discriminatorKey: 'kind' });
const Event = db.model('Test', eventSchema); Event.discriminator('Clicked', Schema({ url: String }));
const doc = new Event({ title: 'foo' });
return doc.validate().then(() => assert.ok(false), err => { assert.ok(err); assert.ok(err.errors['kind']); assert.ok(err.errors['kind'].message.indexOf('required') !== -1, err.errors['kind'].message); }); });
it('does not project in embedded discriminator key if it is the only selected field (gh-7574)', async function() { const sectionSchema = Schema({ title: String }, { discriminatorKey: 'kind' }); const imageSectionSchema = Schema({ href: String }); const textSectionSchema = Schema({ text: String });
const documentSchema = Schema({ title: String, sections: [sectionSchema] });
const sectionsType = documentSchema.path('sections'); sectionsType.discriminator('image', imageSectionSchema); sectionsType.discriminator('text', textSectionSchema);
const Model = db.model('Test', documentSchema);
await Model.create({ title: 'example', sections: [ { kind: 'image', title: 'image', href: 'foo' }, { kind: 'text', title: 'text', text: 'bar' } ] });
let doc = await Model.findOne({}).select('title'); assert.ok(!doc.sections);
doc = await Model.findOne({}).select('title sections.title'); assert.ok(doc.sections); assert.equal(doc.sections[0].kind, 'image'); assert.equal(doc.sections[1].kind, 'text'); });
it('merges schemas instead of overwriting (gh-7884)', function() { const opts = { discriminatorKey: 'kind' };
const eventSchema = Schema({ lookups: [{ name: String }] }, opts); const Event = db.model('Test', eventSchema);
const ClickedLinkEvent = Event.discriminator('Clicked', Schema({ lookups: [{ hi: String }], url: String }, opts));
const e = new ClickedLinkEvent({ lookups: [{ hi: 'address1', name: 'address2' }], url: 'google.com' }); assert.equal(e.lookups.length, 1); assert.equal(e.lookups[0].hi, 'address1'); assert.equal(e.get('lookups.0.name'), 'address2'); assert.equal(e.lookups[0].name, 'address2'); });
it('_id: false in discriminator nested schema (gh-8274)', function() { const schema = new Schema({ operations: { type: [{ _id: Number, action: String }] } }); schema.path('operations').discriminator('Pitch', new Schema({ pitchPath: Schema({ _id: Number, path: [{ _id: false, x: Number, y: Number }] }) })); const Model = db.model('Test', schema);
const doc = new Model(); doc.operations.push({ _id: 42, __t: 'Pitch', pitchPath: { path: [{ x: 1, y: 2 }] } }); assert.strictEqual(doc.operations[0].pitchPath.path[0]._id, void 0); });
it('with discriminators in embedded arrays (gh-8273)', function(done) { const ProductSchema = new Schema({ title: String }); const Product = db.model('Product', ProductSchema); const ProductItemSchema = new Schema({ product: { type: Schema.Types.ObjectId, ref: 'Product' } });
const OrderItemSchema = new Schema({}, { discriminatorKey: '__t' });
const OrderSchema = new Schema({ items: [OrderItemSchema] });
OrderSchema.path('items').discriminator('ProductItem', ProductItemSchema); const Order = db.model('Order', OrderSchema);
const product = new Product({ title: 'Product title' });
const order = new Order({ items: [{ __t: 'ProductItem', product: product }] }); assert.ok(order.items[0].product.title); assert.equal(order.populated('items.product').length, 1);
done(); }); });
it('attempting to populate on base model a virtual path defined on discriminator does not throw an error (gh-8924)', async function() { const User = db.model('User', {}); const Post = db.model('Post', {});
const userWithPostSchema = new Schema({ postId: Schema.ObjectId });
userWithPostSchema.virtual('post', { ref: 'Post', localField: 'postId', foreignField: '_id' });
const UserWithPost = User.discriminator('UserWithPost', userWithPostSchema);
const post = await Post.create({});
await UserWithPost.create({ postId: post._id });
const user = await User.findOne().populate({ path: 'post' });
assert.ok(user.postId); });
it('accepts a POJO as a schema for discriminators (gh-8984)', function() { const User = db.model('User', {}); const SuperUser = User.discriminator('SuperUser', { ability: String });
assert.ok(SuperUser.schema.path('ability')); });
it('removes paths underneath mixed type if discriminator schema sets path to mixed (gh-9042)', function() { const TestSchema = Schema({ name: String }); const MainSchema = Schema({ run: { tab: TestSchema } }, { discriminatorKey: 'type' }); const Main = db.model('Test', MainSchema);
const DiscriminatorSchema = Schema({ run: {} });
const D = Main.discriminator('copy', DiscriminatorSchema); assert.ok(!D.schema.paths['run.tab']);
const doc = new D({ run: { tab: { id: 42 } } }); assert.ifError(doc.validateSync()); });
it('doesnt remove paths at the same level (gh-9362)', function() { const StepSchema = new Schema({ view: { url: { type: String, trim: true } } }, { discriminatorKey: 'type' });
const ClickSchema = new Schema([ StepSchema, { view: { clickCount: { type: Number, default: 1, min: 1 } } } ], { discriminatorKey: 'type' });
const Test = db.model('Test', StepSchema); const D = Test.discriminator('Test1', ClickSchema); assert.ok(D.schema.paths['view.url']);
const doc = new D({ view: { url: 'google.com' } }); assert.ifError(doc.validateSync());
assert.equal(doc.view.url, 'google.com'); assert.equal(doc.view.clickCount, 1); });
it('overwrites if discriminator schema sets a path to single nested but base schema sets to doc array (gh-9354)', function() { const A = db.model('Test', Schema({ prop: [{ reqProp: { type: String, required: true } }] }));
const B = A.discriminator('Test2', Schema({ prop: Schema({ name: String }) }));
assert.ok(!B.schema.path('prop').schema.path('reqProp'));
const doc = new B({ prop: { name: 'test' } }); return doc.validate(); });
it('can use compiled model schema as a discriminator (gh-9238)', function() { const SmsSchema = new mongoose.Schema({ senderNumber: String }); const EmailSchema = new mongoose.Schema({ fromEmailAddress: String }); const messageSchema = new mongoose.Schema({ method: String }, { discriminatorKey: 'method' });
const Message = db.model('Test', messageSchema); Message.discriminator('email', EmailSchema); Message.discriminator('sms', SmsSchema);
const schema = new mongoose.Schema({ actions: [{ name: String }] }); const actions = schema.path('actions');
actions.discriminator('message', Message.schema); assert.ok(actions.schema.discriminators['message']); });
it('embedded discriminator array of arrays (gh-9984)', async function() { const enemySchema = new Schema({ name: String, level: Number }); const Enemy = db.model('Enemy', enemySchema);
const mapSchema = new Schema({ tiles: [[new Schema({}, { discriminatorKey: 'kind', _id: false })]] });
const contentPath = mapSchema.path('tiles');
contentPath.discriminator('Enemy', new Schema({ enemy: { type: Schema.Types.ObjectId, ref: 'Enemy' } })); contentPath.discriminator('Wall', new Schema({ color: String }));
const Map = db.model('Map', mapSchema);

const e = await Enemy.create({ name: 'Bowser', level: 10 });
let map = await Map.create({ tiles: [[{ kind: 'Enemy', enemy: e._id }, { kind: 'Wall', color: 'Blue' }]] });
map = await Map.findById(map).populate({ path: 'tiles.enemy' }); assert.equal(map.tiles[0][0].enemy.name, 'Bowser'); });
it('recursive embedded discriminator using schematype (gh-9600)', function() { const contentSchema = new mongoose.Schema({}, { discriminatorKey: 'type' }); const nestedSchema = new mongoose.Schema({ body: { children: [contentSchema] } }); const childrenArraySchema = nestedSchema.path('body.children'); childrenArraySchema.discriminator( 'container', new mongoose.Schema({ body: { children: childrenArraySchema } }), { clone: false } ); const Nested = mongoose.model('nested', nestedSchema);
const nestedDocument = new Nested({ body: { children: [ { type: 'container', body: { children: [] } }, { type: 'container', body: { children: [ { type: 'container', body: { children: [{ type: 'container', body: { children: [] } }] } } ] } } ] } });
assert.deepEqual(nestedDocument.body.children[1].body.children[0].body.children[0].body.children, []); });
describe('Discriminator Key test', function() { it('gh-9015', async function() { const baseSchema = new Schema({}, { discriminatorKey: 'type' }); const baseModel = db.model('thing', baseSchema); const aSchema = new Schema( { aThing: { type: Number } }, { _id: false, id: false } ); baseModel.discriminator('A', aSchema); const bSchema = new Schema( { bThing: { type: String } }, { _id: false, id: false } ); baseModel.discriminator('B', bSchema); // Model is created as a type A let doc = await baseModel.create({ type: 'A', aThing: 1 }); let res = await baseModel.findByIdAndUpdate( doc._id, { type: 'B', bThing: 'one', aThing: '2' }, { runValidators: true, /* overwriteDiscriminatorKey: true, */ new: true } ); assert.equal(res.type, 'A');
doc = await baseModel.create({ type: 'A', aThing: 1 }); res = await baseModel.findByIdAndUpdate( doc._id, { type: 'B', bThing: 'one', aThing: '2' }, { runValidators: true, overwriteDiscriminatorKey: true, new: true } ); assert.equal(res.type, 'B'); }); });
it('takes discriminator schema\'s single nested over base schema\'s (gh-10157)', function() { const personSchema = new Schema({ name: Schema({ firstName: String, lastName: String }), kind: { type: 'String', enum: ['normal', 'vip'], required: true } }, { discriminatorKey: 'kind' });
const Person = db.model('Person', personSchema);
const vipSchema = Schema({ name: Schema({ firstName: { type: 'String', required: true }, title: { type: 'String', required: true } }) }); const Vip = Person.discriminator('vip', vipSchema);
const doc1 = new Vip({ name: { firstName: 'John' } }); let err = doc1.validateSync(); assert.ok(err); assert.ok(err.errors['name.title']);
const doc2 = new Vip({ name: { title: 'Dr' } }); err = doc2.validateSync(); assert.ok(err); assert.ok(err.errors['name.firstName']); });
it('allows using array as tied value (gh-10303)', function() { const mongooseSchema = new mongoose.Schema({ tenantRefs: [{ type: String, required: true }], otherData: { type: String, required: true } }, { discriminatorKey: 'tenantRefs' }); const Model = db.model('Test', mongooseSchema);
const D = Model.discriminator('D', Schema({}), ['abc', '123']);
return D.create({ otherData: 'test', tenantRefs: ['abc', '123'] }).then(res => { assert.deepEqual(res.toObject().tenantRefs, ['abc', '123']); }); });
it('handles nested discriminators (gh-10702)', async function() { const GrandChildSchema1 = new mongoose.Schema({ gc11: String, gc12: Number });
const GrandChildSchema2 = new mongoose.Schema({ gc21: Number, gc22: Number });
const ParentSchema = new mongoose.Schema({ top: String, c: Schema({}) });
const ChildSchema1 = Schema({ str: String, num: Number, gc: Schema({}) });
ParentSchema.path('c').discriminator('Child1', ChildSchema1, { clone: false }); ParentSchema.path('c').discriminator('Child2', Schema({ anotherStr: String }));
ChildSchema1.path('gc').discriminator('GrandChild1', GrandChildSchema1); ChildSchema1.path('gc').discriminator('GrandChild2', GrandChildSchema2);
const Parent = db.model('Parent', ParentSchema);
const p = await Parent.create({ top: 'Top', c: { __t: 'Child1', str: 'string', num: 3, gc: { __t: 'GrandChild1', gc11: 'eleventy', gc12: 110 } } }); const res = await Parent.findById(p); assert.equal(res.c.gc.gc11, 'eleventy'); assert.equal(res.c.gc.gc12, 110); });
it('Should allow reusing discriminators (gh-10931)', async function() { const options = { discriminatorKey: 'kind', overwriteModels: true }; const eventSchema = new mongoose.Schema({ time: Date }, options); const Event = db.model('Event', eventSchema);
const ClickedLinkEvent = Event.discriminator('ClickedLink', new mongoose.Schema({ url: String }, options)); const reUse = Event.discriminator('ClickedLink', new mongoose.Schema({ url: String }, options)); assert.ok(reUse);

const genericEvent = new Event({ time: Date.now(), url: 'google.com' }); assert.ok(!genericEvent.url);
const clickedEvent = new ClickedLinkEvent({ time: Date.now(), url: 'google.com' }); assert.ok(clickedEvent.url);
const reUseEvent = new reUse({ time: Date.now(), url: 'test.com' }); assert.ok(reUseEvent.url); });
it('handles updating multiple properties nested underneath a discriminator (gh-11428)', async function() { const StepSchema = new Schema({ url: String }, { discriminatorKey: 'type' }); const SheetReadOptionsSchema = new Schema({ location: { id: String, timeout: Number } }); const ClickSchema = new Schema( [StepSchema, { sheetOptions: SheetReadOptionsSchema }], { discriminatorKey: 'type' } );
const Step = db.model('Step', StepSchema);
Step.discriminator('click', ClickSchema);
const doc = await Step.create({ type: 'click', url: 'https://google.com', sheetOptions: { location: { id: 'currentCell', timeout: 1000 } } });
doc.set({ 'sheetOptions.location.id': 'inColumn', 'sheetOptions.location.timeout': 2000 }); await doc.save();
const updatedDoc = await Step.findOne({ type: 'click', _id: doc._id }); assert.equal(updatedDoc.sheetOptions.location.id, 'inColumn'); assert.equal(updatedDoc.sheetOptions.location.timeout, 2000); });
it('allows defining discriminator at the subSchema level in the subschema (gh-7971)', async function() { const eventSchema = new Schema({ message: String }, { discriminatorKey: 'kind', _id: false }); const clickedSchema = new Schema({ element: { type: String, required: true } }, { _id: false }); const batchSchema = new Schema({ events: [eventSchema], mainEvent: { type: eventSchema, discriminators: { Clicked: clickedSchema } } }); const arraySchema = new Schema({ arrayEvent: [{ type: eventSchema, discriminators: { Clicked: clickedSchema } }] }); const Batch = db.model('Batch', batchSchema); const Arrays = db.model('Array', arraySchema);
const batch = await Batch.create({ events: [{ message: 'Hello World' }], mainEvent: { message: 'Goodbye', element: 'The Discriminator', kind: 'Clicked' } });
assert(batch.mainEvent.element);
const array = await Arrays.create({ arrayEvent: [{ message: 'An array', element: 'with discriminators', kind: 'Clicked' }] });
assert(array.arrayEvent[0].element); });
it('handles discriminators on maps of subdocuments (gh-11720)', async function() { const shapeSchema = Schema({ name: String }, { discriminatorKey: 'kind' }); const schema = Schema({ shape: { type: Map, of: shapeSchema } });
schema.path('shape.$*').discriminator('Circle', Schema({ radius: String })); schema.path('shape.$*').discriminator('Square', Schema({ side: Number }));
const Test = db.model('Test', schema);
let doc = new Test({ shape: { a: { kind: 'Circle', radius: 5 }, b: { kind: 'Square', side: 10 } } });
assert.strictEqual(doc.shape.get('a').radius, '5'); assert.strictEqual(doc.shape.get('b').side, 10);
await doc.save();
doc = await Test.findById(doc);
assert.strictEqual(doc.shape.get('a').radius, '5'); assert.strictEqual(doc.shape.get('b').side, 10); });
it('supports `mergeHooks` option to use the discriminator schema\'s hooks over the base schema\'s (gh-12472)', function() { const shapeSchema = Schema({ name: String }, { discriminatorKey: 'kind' }); shapeSchema.plugin(myPlugin);
const Shape = db.model('Test', shapeSchema);
const triangleSchema = Schema({ sides: { type: Number, enum: [3] } }); triangleSchema.plugin(myPlugin); const Triangle = Shape.discriminator( 'Triangle', triangleSchema ); const squareSchema = Schema({ sides: { type: Number, enum: [4] } }); squareSchema.plugin(myPlugin); const Square = Shape.discriminator( 'Square', squareSchema, { mergeHooks: false } );
assert.equal(Triangle.schema.s.hooks._pres.get('save').filter(hook => hook.fn.name === 'testHook12472').length, 2); assert.equal(Square.schema.s.hooks._pres.get('save').filter(hook => hook.fn.name === 'testHook12472').length, 1);
function myPlugin(schema) { schema.pre('save', function testHook12472() {}); } });
it('supports `mergePlugins` option to use the discriminator schema\'s plugins over the base schema\'s (gh-12604)', function() { let pluginTimes = 0; const shapeDef = { name: String }; const shapeSchema = Schema(shapeDef, { discriminatorKey: 'kind' }); shapeSchema.plugin(myPlugin, { opts1: true });
const Shape = db.model('Test', shapeSchema);
const triangleSchema = Schema({ ...shapeDef, sides: { type: Number, enum: [3] } }); triangleSchema.plugin(myPlugin, { opts2: true }); const Triangle = Shape.discriminator( 'Triangle', triangleSchema );
const squareSchema = Schema({ ...shapeDef, sides: { type: Number, enum: [4] } }); squareSchema.plugin(myPlugin, { opts3: true }); const Square = Shape.discriminator( 'Square', squareSchema, { mergeHooks: false, mergePlugins: false } );
assert.equal(Triangle.schema.s.hooks._pres.get('save').filter(hook => hook.fn.name === 'testHook12604').length, 2); assert.equal(Square.schema.s.hooks._pres.get('save').filter(hook => hook.fn.name === 'testHook12604').length, 1);
const squareFilteredPlugins = Square.schema.plugins.filter((obj) => obj.fn.name === 'myPlugin'); assert.equal(squareFilteredPlugins.length, 1); assert.equal(squareFilteredPlugins[0].opts['opts3'], true);
assert.equal(pluginTimes, 3);
function myPlugin(schema) { pluginTimes += 1; schema.pre('save', function testHook12604() {}); } });});