2019-02-01 14:06:44 +01:00
'use strict' ;
/ * !
* Module dependencies .
* /
const EventEmitter = require ( 'events' ) . EventEmitter ;
const InternalCache = require ( './internal' ) ;
const MongooseError = require ( './error' ) ;
const MixedSchema = require ( './schema/mixed' ) ;
const ObjectExpectedError = require ( './error/objectExpected' ) ;
const ObjectParameterError = require ( './error/objectParameter' ) ;
const StrictModeError = require ( './error/strict' ) ;
const ValidatorError = require ( './schematype' ) . ValidatorError ;
const VirtualType = require ( './virtualtype' ) ;
const cleanModifiedSubpaths = require ( './helpers/document/cleanModifiedSubpaths' ) ;
const compile = require ( './helpers/document/compile' ) . compile ;
const defineKey = require ( './helpers/document/compile' ) . defineKey ;
const flatten = require ( './helpers/common' ) . flatten ;
const get = require ( './helpers/get' ) ;
const getEmbeddedDiscriminatorPath = require ( './helpers/document/getEmbeddedDiscriminatorPath' ) ;
const idGetter = require ( './plugins/idGetter' ) ;
const isDefiningProjection = require ( './helpers/projection/isDefiningProjection' ) ;
const isExclusive = require ( './helpers/projection/isExclusive' ) ;
const inspect = require ( 'util' ) . inspect ;
const internalToObjectOptions = require ( './options' ) . internalToObjectOptions ;
const mpath = require ( 'mpath' ) ;
const utils = require ( './utils' ) ;
const ValidationError = MongooseError . ValidationError ;
const clone = utils . clone ;
const deepEqual = utils . deepEqual ;
const isMongooseObject = utils . isMongooseObject ;
2019-06-04 14:29:48 +02:00
const arrayAtomicsSymbol = require ( './helpers/symbols' ) . arrayAtomicsSymbol ;
2019-02-01 14:06:44 +01:00
const documentArrayParent = require ( './helpers/symbols' ) . documentArrayParent ;
2019-07-02 16:05:15 +02:00
const documentSchemaSymbol = require ( './helpers/symbols' ) . documentSchemaSymbol ;
2019-02-01 14:06:44 +01:00
const getSymbol = require ( './helpers/symbols' ) . getSymbol ;
2019-07-02 16:05:15 +02:00
const populateModelSymbol = require ( './helpers/symbols' ) . populateModelSymbol ;
2019-02-01 14:06:44 +01:00
let DocumentArray ;
let MongooseArray ;
let Embedded ;
const specialProperties = utils . specialProperties ;
/ * *
* The core Mongoose document constructor . You should not call this directly ,
* the Mongoose [ Model constructor ] ( . / api . html # Model ) calls this for you .
*
* @ param { Object } obj the values to set
* @ param { Object } [ fields ] optional object containing the fields which were selected in the query returning this document and any populated paths data
* @ param { Boolean } [ skipId ] bool , should we auto create an ObjectId _id
* @ inherits NodeJS EventEmitter http : //nodejs.org/api/events.html#events_class_events_eventemitter
* @ event ` init ` : Emitted on a document after it has was retreived from the db and fully hydrated by Mongoose .
* @ event ` save ` : Emitted when the document is successfully saved
* @ api private
* /
function Document ( obj , fields , skipId , options ) {
if ( typeof skipId === 'object' && skipId != null ) {
options = skipId ;
skipId = options . skipId ;
}
options = options || { } ;
this . $ _ _ = new InternalCache ;
this . $ _ _ . emitter = new EventEmitter ( ) ;
this . isNew = 'isNew' in options ? options . isNew : true ;
this . errors = undefined ;
this . $ _ _ . $options = options || { } ;
if ( obj != null && typeof obj !== 'object' ) {
throw new ObjectParameterError ( obj , 'obj' , 'Document' ) ;
}
const schema = this . schema ;
if ( typeof fields === 'boolean' ) {
this . $ _ _ . strictMode = fields ;
fields = undefined ;
} else {
this . $ _ _ . strictMode = schema . options . strict ;
this . $ _ _ . selected = fields ;
}
const required = schema . requiredPaths ( true ) ;
for ( let i = 0 ; i < required . length ; ++ i ) {
this . $ _ _ . activePaths . require ( required [ i ] ) ;
}
this . $ _ _ . emitter . setMaxListeners ( 0 ) ;
let exclude = null ;
// determine if this doc is a result of a query with
// excluded fields
2019-06-04 14:29:48 +02:00
if ( utils . isPOJO ( fields ) ) {
2019-02-01 14:06:44 +01:00
exclude = isExclusive ( fields ) ;
}
const hasIncludedChildren = exclude === false && fields ?
$ _ _hasIncludedChildren ( fields ) :
{ } ;
2019-06-04 14:29:48 +02:00
if ( this . _doc == null ) {
this . $ _ _buildDoc ( obj , fields , skipId , exclude , hasIncludedChildren , false ) ;
2019-02-01 14:06:44 +01:00
2019-06-04 14:29:48 +02:00
// By default, defaults get applied **before** setting initial values
// Re: gh-6155
$ _ _applyDefaults ( this , fields , skipId , exclude , hasIncludedChildren , true , {
isNew : this . isNew
} ) ;
}
2019-02-01 14:06:44 +01:00
if ( obj ) {
if ( obj instanceof Document ) {
this . isNew = obj . isNew ;
}
// Skip set hooks
if ( this . $ _ _original _set ) {
this . $ _ _original _set ( obj , undefined , true ) ;
} else {
this . $set ( obj , undefined , true ) ;
}
}
// Function defaults get applied **after** setting initial values so they
// see the full doc rather than an empty one, unless they opt out.
// Re: gh-3781, gh-6155
if ( options . willInit ) {
this . once ( 'init' , ( ) => {
$ _ _applyDefaults ( this , fields , skipId , exclude , hasIncludedChildren , false , options . skipDefaults , {
isNew : this . isNew
} ) ;
} ) ;
} else {
$ _ _applyDefaults ( this , fields , skipId , exclude , hasIncludedChildren , false , options . skipDefaults , {
isNew : this . isNew
} ) ;
}
this . $ _ _ . _id = this . _id ;
2019-06-04 14:29:48 +02:00
this . $locals = { } ;
2019-02-01 14:06:44 +01:00
if ( ! schema . options . strict && obj ) {
const _this = this ;
const keys = Object . keys ( this . _doc ) ;
keys . forEach ( function ( key ) {
if ( ! ( key in schema . tree ) ) {
defineKey ( key , null , _this ) ;
}
} ) ;
}
applyQueue ( this ) ;
}
/ * !
* Document exposes the NodeJS event emitter API , so you can use
* ` on ` , ` once ` , etc .
* /
utils . each (
[ 'on' , 'once' , 'emit' , 'listeners' , 'removeListener' , 'setMaxListeners' ,
'removeAllListeners' , 'addListener' ] ,
function ( emitterFn ) {
Document . prototype [ emitterFn ] = function ( ) {
return this . $ _ _ . emitter [ emitterFn ] . apply ( this . $ _ _ . emitter , arguments ) ;
} ;
} ) ;
Document . prototype . constructor = Document ;
/ * *
* The documents schema .
*
* @ api public
* @ property schema
* @ memberOf Document
* @ instance
* /
Document . prototype . schema ;
2019-06-04 14:29:48 +02:00
/ * *
* Empty object that you can use for storing properties on the document . This
* is handy for passing data to middleware without conflicting with Mongoose
* internals .
*
* # # # # Example :
*
* schema . pre ( 'save' , function ( ) {
* // Mongoose will set `isNew` to `false` if `save()` succeeds
* this . $locals . wasNew = this . isNew ;
* } ) ;
*
* schema . post ( 'save' , function ( ) {
* // Prints true if `isNew` was set before `save()`
* console . log ( this . $locals . wasNew ) ;
* } ) ;
*
* @ api public
* @ property $locals
* @ memberOf Document
* @ instance
* /
Object . defineProperty ( Document . prototype , '$locals' , {
configurable : false ,
enumerable : false ,
writable : true
} ) ;
2019-02-01 14:06:44 +01:00
/ * *
* Boolean flag specifying if the document is new .
*
* @ api public
* @ property isNew
* @ memberOf Document
* @ instance
* /
Document . prototype . isNew ;
/ * *
* The string version of this documents _id .
*
* # # # # Note :
*
* This getter exists on all documents by default . The getter can be disabled by setting the ` id ` [ option ] ( / d o c s / g u i d e . h t m l # i d ) o f i t s ` S c h e m a ` t o f a l s e a t c o n s t r u c t i o n t i m e .
*
* new Schema ( { name : String } , { id : false } ) ;
*
* @ api public
* @ see Schema options / docs / guide . html # options
* @ property id
* @ memberOf Document
* @ instance
* /
Document . prototype . id ;
/ * *
* Hash containing current validation errors .
*
* @ api public
* @ property errors
* @ memberOf Document
* @ instance
* /
Document . prototype . errors ;
/ * !
* ignore
* /
function $ _ _hasIncludedChildren ( fields ) {
const hasIncludedChildren = { } ;
const keys = Object . keys ( fields ) ;
for ( let j = 0 ; j < keys . length ; ++ j ) {
const parts = keys [ j ] . split ( '.' ) ;
const c = [ ] ;
for ( let k = 0 ; k < parts . length ; ++ k ) {
c . push ( parts [ k ] ) ;
hasIncludedChildren [ c . join ( '.' ) ] = 1 ;
}
}
return hasIncludedChildren ;
}
/ * !
* ignore
* /
function $ _ _applyDefaults ( doc , fields , skipId , exclude , hasIncludedChildren , isBeforeSetters , pathsToSkip ) {
const paths = Object . keys ( doc . schema . paths ) ;
const plen = paths . length ;
for ( let i = 0 ; i < plen ; ++ i ) {
let def ;
let curPath = '' ;
const p = paths [ i ] ;
if ( p === '_id' && skipId ) {
continue ;
}
const type = doc . schema . paths [ p ] ;
const path = p . split ( '.' ) ;
const len = path . length ;
let included = false ;
let doc _ = doc . _doc ;
for ( let j = 0 ; j < len ; ++ j ) {
if ( doc _ == null ) {
break ;
}
const piece = path [ j ] ;
curPath += ( ! curPath . length ? '' : '.' ) + piece ;
if ( exclude === true ) {
if ( curPath in fields ) {
break ;
}
} else if ( exclude === false && fields && ! included ) {
if ( curPath in fields ) {
included = true ;
} else if ( ! hasIncludedChildren [ curPath ] ) {
break ;
}
}
if ( j === len - 1 ) {
if ( doc _ [ piece ] !== void 0 ) {
break ;
}
if ( typeof type . defaultValue === 'function' ) {
if ( ! type . defaultValue . $runBeforeSetters && isBeforeSetters ) {
break ;
}
if ( type . defaultValue . $runBeforeSetters && ! isBeforeSetters ) {
break ;
}
} else if ( ! isBeforeSetters ) {
// Non-function defaults should always run **before** setters
continue ;
}
if ( pathsToSkip && pathsToSkip [ curPath ] ) {
break ;
}
if ( fields && exclude !== null ) {
if ( exclude === true ) {
// apply defaults to all non-excluded fields
if ( p in fields ) {
continue ;
}
def = type . getDefault ( doc , false ) ;
if ( typeof def !== 'undefined' ) {
doc _ [ piece ] = def ;
doc . $ _ _ . activePaths . default ( p ) ;
}
} else if ( included ) {
// selected field
def = type . getDefault ( doc , false ) ;
if ( typeof def !== 'undefined' ) {
doc _ [ piece ] = def ;
doc . $ _ _ . activePaths . default ( p ) ;
}
}
} else {
def = type . getDefault ( doc , false ) ;
if ( typeof def !== 'undefined' ) {
doc _ [ piece ] = def ;
doc . $ _ _ . activePaths . default ( p ) ;
}
}
} else {
doc _ = doc _ [ piece ] ;
}
}
}
}
/ * *
* Builds the default doc structure
*
* @ param { Object } obj
* @ param { Object } [ fields ]
* @ param { Boolean } [ skipId ]
* @ api private
* @ method $ _ _buildDoc
* @ memberOf Document
* @ instance
* /
Document . prototype . $ _ _buildDoc = function ( obj , fields , skipId , exclude , hasIncludedChildren ) {
const doc = { } ;
const paths = Object . keys ( this . schema . paths ) .
// Don't build up any paths that are underneath a map, we don't know
// what the keys will be
filter ( p => ! p . includes ( '$*' ) ) ;
const plen = paths . length ;
let ii = 0 ;
for ( ; ii < plen ; ++ ii ) {
const p = paths [ ii ] ;
if ( p === '_id' ) {
if ( skipId ) {
continue ;
}
if ( obj && '_id' in obj ) {
continue ;
}
}
const path = p . split ( '.' ) ;
const len = path . length ;
const last = len - 1 ;
let curPath = '' ;
let doc _ = doc ;
let included = false ;
for ( let i = 0 ; i < len ; ++ i ) {
const piece = path [ i ] ;
curPath += ( ! curPath . length ? '' : '.' ) + piece ;
// support excluding intermediary levels
if ( exclude === true ) {
if ( curPath in fields ) {
break ;
}
} else if ( exclude === false && fields && ! included ) {
if ( curPath in fields ) {
included = true ;
} else if ( ! hasIncludedChildren [ curPath ] ) {
break ;
}
}
if ( i < last ) {
doc _ = doc _ [ piece ] || ( doc _ [ piece ] = { } ) ;
}
}
}
this . _doc = doc ;
} ;
/ * !
* Converts to POJO when you use the document for querying
* /
Document . prototype . toBSON = function ( ) {
return this . toObject ( internalToObjectOptions ) ;
} ;
/ * *
* Initializes the document without setters or marking anything modified .
*
* Called internally after a document is returned from mongodb . Normally ,
* you do * * not * * need to call this function on your own .
*
* This function triggers ` init ` [ middleware ] ( / d o c s / m i d d l e w a r e . h t m l ) .
* Note that ` init ` hooks are [ synchronous ] ( / d o c s / m i d d l e w a r e . h t m l # s y n c h r o n o u s ) .
*
* @ param { Object } doc document returned by mongo
* @ api public
* @ memberOf Document
* @ instance
* /
Document . prototype . init = function ( doc , opts , fn ) {
if ( typeof opts === 'function' ) {
fn = opts ;
opts = null ;
}
this . $ _ _init ( doc , opts ) ;
if ( fn ) {
fn ( null , this ) ;
}
return this ;
} ;
/ * !
* ignore
* /
Document . prototype . $ _ _init = function ( doc , opts ) {
this . isNew = false ;
this . $init = true ;
// handle docs with populated paths
// If doc._id is not null or undefined
if ( doc . _id !== null && doc . _id !== undefined &&
opts && opts . populated && opts . populated . length ) {
const id = String ( doc . _id ) ;
for ( let i = 0 ; i < opts . populated . length ; ++ i ) {
const item = opts . populated [ i ] ;
if ( item . isVirtual ) {
this . populated ( item . path , utils . getValue ( item . path , doc ) , item ) ;
} else {
this . populated ( item . path , item . _docs [ id ] , item ) ;
}
}
}
init ( this , doc , this . _doc ) ;
this . emit ( 'init' , this ) ;
this . constructor . emit ( 'init' , this ) ;
this . $ _ _ . _id = this . _id ;
return this ;
} ;
/ * !
* Init helper .
*
* @ param { Object } self document instance
* @ param { Object } obj raw mongodb doc
* @ param { Object } doc object we are initializing
* @ api private
* /
function init ( self , obj , doc , prefix ) {
prefix = prefix || '' ;
const keys = Object . keys ( obj ) ;
const len = keys . length ;
let schema ;
let path ;
let i ;
let index = 0 ;
while ( index < len ) {
_init ( index ++ ) ;
}
function _init ( index ) {
i = keys [ index ] ;
path = prefix + i ;
schema = self . schema . path ( path ) ;
// Should still work if not a model-level discriminator, but should not be
// necessary. This is *only* to catch the case where we queried using the
// base model and the discriminated model has a projection
if ( self . schema . $isRootDiscriminator && ! self . isSelected ( path ) ) {
return ;
}
2019-06-04 14:29:48 +02:00
if ( ! schema && utils . isPOJO ( obj [ i ] ) ) {
2019-02-01 14:06:44 +01:00
// assume nested object
if ( ! doc [ i ] ) {
doc [ i ] = { } ;
}
init ( self , obj [ i ] , doc [ i ] , path + '.' ) ;
} else if ( ! schema ) {
doc [ i ] = obj [ i ] ;
} else {
if ( obj [ i ] === null ) {
doc [ i ] = null ;
} else if ( obj [ i ] !== undefined ) {
const intCache = obj [ i ] . $ _ _ || { } ;
const wasPopulated = intCache . wasPopulated || null ;
if ( schema && ! wasPopulated ) {
try {
doc [ i ] = schema . cast ( obj [ i ] , self , true ) ;
} catch ( e ) {
self . invalidate ( e . path , new ValidatorError ( {
path : e . path ,
message : e . message ,
type : 'cast' ,
value : e . value
} ) ) ;
}
} else {
doc [ i ] = obj [ i ] ;
}
}
// mark as hydrated
if ( ! self . isModified ( path ) ) {
self . $ _ _ . activePaths . init ( path ) ;
}
}
}
}
/ * *
* Sends an update command with this document ` _id ` as the query selector .
*
* # # # # Example :
*
* weirdCar . update ( { $inc : { wheels : 1 } } , { w : 1 } , callback ) ;
*
* # # # # Valid options :
*
* - same as in [ Model . update ] ( # model _Model . update )
*
* @ see Model . update # model _Model . update
* @ param { Object } doc
* @ param { Object } options
* @ param { Function } callback
* @ return { Query }
* @ api public
* @ memberOf Document
* @ instance
* /
Document . prototype . update = function update ( ) {
const args = utils . args ( arguments ) ;
args . unshift ( { _id : this . _id } ) ;
2019-04-17 15:58:15 +02:00
const query = this . constructor . update . apply ( this . constructor , args ) ;
if ( this . $session ( ) != null ) {
if ( ! ( 'session' in query . options ) ) {
query . options . session = this . $session ( ) ;
}
}
return query ;
2019-02-01 14:06:44 +01:00
} ;
/ * *
* Sends an updateOne command with this document ` _id ` as the query selector .
*
* # # # # Example :
*
* weirdCar . updateOne ( { $inc : { wheels : 1 } } , { w : 1 } , callback ) ;
*
* # # # # Valid options :
*
* - same as in [ Model . updateOne ] ( # model _Model . updateOne )
*
* @ see Model . updateOne # model _Model . updateOne
* @ param { Object } doc
* @ param { Object } options
* @ param { Function } callback
* @ return { Query }
* @ api public
* @ memberOf Document
* @ instance
* /
Document . prototype . updateOne = function updateOne ( doc , options , callback ) {
const query = this . constructor . updateOne ( { _id : this . _id } , doc , options ) ;
query . _pre ( cb => {
this . constructor . _middleware . execPre ( 'updateOne' , this , [ ] , cb ) ;
} ) ;
query . _post ( cb => {
this . constructor . _middleware . execPost ( 'updateOne' , this , [ ] , { } , cb ) ;
} ) ;
2019-04-17 15:58:15 +02:00
if ( this . $session ( ) != null ) {
if ( ! ( 'session' in query . options ) ) {
query . options . session = this . $session ( ) ;
}
}
2019-02-01 14:06:44 +01:00
if ( callback != null ) {
return query . exec ( callback ) ;
}
return query ;
} ;
/ * *
* Sends a replaceOne command with this document ` _id ` as the query selector .
*
* # # # # Valid options :
*
* - same as in [ Model . replaceOne ] ( # model _Model . replaceOne )
*
* @ see Model . replaceOne # model _Model . replaceOne
* @ param { Object } doc
* @ param { Object } options
* @ param { Function } callback
* @ return { Query }
* @ api public
* @ memberOf Document
* @ instance
* /
Document . prototype . replaceOne = function replaceOne ( ) {
const args = utils . args ( arguments ) ;
args . unshift ( { _id : this . _id } ) ;
return this . constructor . replaceOne . apply ( this . constructor , args ) ;
} ;
/ * *
* Getter / setter around the session associated with this document . Used to
* automatically set ` session ` if you ` save() ` a doc that you got from a
* query with an associated session .
*
* # # # # Example :
*
* const session = MyModel . startSession ( ) ;
* const doc = await MyModel . findOne ( ) . session ( session ) ;
* doc . $session ( ) === session ; // true
* doc . $session ( null ) ;
* doc . $session ( ) === null ; // true
*
* If this is a top - level document , setting the session propagates to all child
* docs .
*
* @ param { ClientSession } [ session ] overwrite the current session
* @ return { ClientSession }
* @ method $session
* @ api public
* @ memberOf Document
* /
Document . prototype . $session = function $session ( session ) {
if ( arguments . length === 0 ) {
return this . $ _ _ . session ;
}
this . $ _ _ . session = session ;
if ( ! this . ownerDocument ) {
const subdocs = this . $ _ _getAllSubdocs ( ) ;
for ( const child of subdocs ) {
child . $session ( session ) ;
}
}
return session ;
} ;
2019-07-02 16:05:15 +02:00
/ * *
* Overwrite all values in this document with the values of ` obj ` , except
* for immutable properties . Behaves similarly to ` set() ` , except for it
* unsets all properties that aren ' t in ` obj ` .
*
* @ param { Object } obj the object to overwrite this document with
* @ method overwrite
* @ name overwrite
* @ memberOf Document
* @ instance
* @ api public
* /
Document . prototype . overwrite = function overwrite ( obj ) {
const keys = Array . from ( new Set ( Object . keys ( this . _doc ) . concat ( Object . keys ( obj ) ) ) ) ;
for ( const key of keys ) {
if ( key === '_id' ) {
continue ;
}
// Explicitly skip version key
if ( this . schema . options . versionKey && key === this . schema . options . versionKey ) {
continue ;
}
this . $set ( key , obj [ key ] ) ;
}
return this ;
} ;
2019-02-01 14:06:44 +01:00
/ * *
* Alias for ` set() ` , used internally to avoid conflicts
*
* @ param { String | Object } path path or object of key / vals to set
* @ param { Any } val the value to set
* @ param { Schema | String | Number | Buffer | * } [ type ] optionally specify a type for "on-the-fly" attributes
* @ param { Object } [ options ] optionally specify options that modify the behavior of the set
* @ method $set
* @ name $set
* @ memberOf Document
* @ instance
* @ api public
* /
Document . prototype . $set = function $set ( path , val , type , options ) {
2019-06-04 14:29:48 +02:00
if ( utils . isPOJO ( type ) ) {
2019-02-01 14:06:44 +01:00
options = type ;
type = undefined ;
}
options = options || { } ;
const merge = options . merge ;
const adhoc = type && type !== true ;
const constructing = type === true ;
let adhocs ;
let keys ;
let i = 0 ;
let pathtype ;
let key ;
let prefix ;
const strict = 'strict' in options
? options . strict
: this . $ _ _ . strictMode ;
if ( adhoc ) {
adhocs = this . $ _ _ . adhocPaths || ( this . $ _ _ . adhocPaths = { } ) ;
adhocs [ path ] = this . schema . interpretAsType ( path , type , this . schema . options ) ;
}
if ( typeof path !== 'string' ) {
// new Document({ key: val })
if ( path === null || path === void 0 ) {
const _ = path ;
path = val ;
val = _ ;
} else {
prefix = val ? val + '.' : '' ;
if ( path instanceof Document ) {
if ( path . $ _ _isNested ) {
path = path . toObject ( ) ;
} else {
path = path . _doc ;
}
}
keys = Object . keys ( path ) ;
const len = keys . length ;
if ( len === 0 && ! this . schema . options . minimize ) {
if ( val ) {
this . $set ( val , { } ) ;
}
return this ;
}
while ( i < len ) {
_handleIndex . call ( this , i ++ ) ;
}
return this ;
}
2019-06-04 14:29:48 +02:00
} else {
this . $ _ _ . $setCalled . add ( path ) ;
2019-02-01 14:06:44 +01:00
}
function _handleIndex ( i ) {
key = keys [ i ] ;
const pathName = prefix + key ;
pathtype = this . schema . pathType ( pathName ) ;
// On initial set, delete any nested keys if we're going to overwrite
// them to ensure we keep the user's key order.
if ( type === true &&
! prefix &&
path [ key ] != null &&
pathtype === 'nested' &&
this . _doc [ key ] != null &&
Object . keys ( this . _doc [ key ] ) . length === 0 ) {
delete this . _doc [ key ] ;
}
2019-06-04 14:29:48 +02:00
if ( typeof path [ key ] === 'object' &&
2019-07-02 16:05:15 +02:00
! utils . isNativeObject ( path [ key ] ) &&
2019-06-04 14:29:48 +02:00
path [ key ] != null &&
2019-02-01 14:06:44 +01:00
pathtype !== 'virtual' &&
pathtype !== 'real' &&
! ( this . $ _ _path ( pathName ) instanceof MixedSchema ) &&
! ( this . schema . paths [ pathName ] &&
this . schema . paths [ pathName ] . options &&
this . schema . paths [ pathName ] . options . ref ) ) {
2019-06-04 14:29:48 +02:00
this . $ _ _ . $setCalled . add ( prefix + key ) ;
2019-02-01 14:06:44 +01:00
this . $set ( path [ key ] , prefix + key , constructing ) ;
} else if ( strict ) {
// Don't overwrite defaults with undefined keys (gh-3981)
if ( constructing && path [ key ] === void 0 &&
this . get ( key ) !== void 0 ) {
return ;
}
if ( pathtype === 'adhocOrUndefined' ) {
pathtype = getEmbeddedDiscriminatorPath ( this , pathName , { typeOnly : true } ) ;
}
if ( pathtype === 'real' || pathtype === 'virtual' ) {
// Check for setting single embedded schema to document (gh-3535)
let p = path [ key ] ;
if ( this . schema . paths [ pathName ] &&
this . schema . paths [ pathName ] . $isSingleNested &&
path [ key ] instanceof Document ) {
p = p . toObject ( { virtuals : false , transform : false } ) ;
}
this . $set ( prefix + key , p , constructing ) ;
} else if ( pathtype === 'nested' && path [ key ] instanceof Document ) {
this . $set ( prefix + key ,
path [ key ] . toObject ( { transform : false } ) , constructing ) ;
} else if ( strict === 'throw' ) {
if ( pathtype === 'nested' ) {
throw new ObjectExpectedError ( key , path [ key ] ) ;
} else {
throw new StrictModeError ( key ) ;
}
}
} else if ( path [ key ] !== void 0 ) {
this . $set ( prefix + key , path [ key ] , constructing ) ;
}
}
2019-06-04 14:29:48 +02:00
let pathType = this . schema . pathType ( path ) ;
if ( pathType === 'adhocOrUndefined' ) {
pathType = getEmbeddedDiscriminatorPath ( this , path , { typeOnly : true } ) ;
}
// Assume this is a Mongoose document that was copied into a POJO using
// `Object.assign()` or `{...doc}`
if ( utils . isPOJO ( val ) && val . $ _ _ != null && val . _doc != null ) {
val = val . _doc ;
}
2019-02-01 14:06:44 +01:00
if ( pathType === 'nested' && val ) {
2019-06-04 14:29:48 +02:00
if ( typeof val === 'object' && val != null ) {
2019-02-01 14:06:44 +01:00
if ( ! merge ) {
2019-07-02 16:05:15 +02:00
this . $ _ _setValue ( path , null ) ;
2019-02-01 14:06:44 +01:00
cleanModifiedSubpaths ( this , path ) ;
} else {
return this . $set ( val , path , constructing ) ;
}
const keys = Object . keys ( val ) ;
2019-07-02 16:05:15 +02:00
this . $ _ _setValue ( path , { } ) ;
2019-02-01 14:06:44 +01:00
for ( const key of keys ) {
this . $set ( path + '.' + key , val [ key ] , constructing ) ;
}
this . markModified ( path ) ;
cleanModifiedSubpaths ( this , path , { skipDocArrays : true } ) ;
return this ;
}
this . invalidate ( path , new MongooseError . CastError ( 'Object' , val , path ) ) ;
return this ;
}
let schema ;
const parts = path . split ( '.' ) ;
2019-06-04 14:29:48 +02:00
// Might need to change path for top-level alias
if ( typeof this . schema . aliases [ parts [ 0 ] ] == 'string' ) {
parts [ 0 ] = this . schema . aliases [ parts [ 0 ] ] ;
}
2019-02-01 14:06:44 +01:00
if ( pathType === 'adhocOrUndefined' && strict ) {
// check for roots that are Mixed types
let mixed ;
for ( i = 0 ; i < parts . length ; ++ i ) {
const subpath = parts . slice ( 0 , i + 1 ) . join ( '.' ) ;
// If path is underneath a virtual, bypass everything and just set it.
if ( i + 1 < parts . length && this . schema . pathType ( subpath ) === 'virtual' ) {
mpath . set ( path , val , this ) ;
return this ;
}
schema = this . schema . path ( subpath ) ;
if ( schema == null ) {
continue ;
}
if ( schema instanceof MixedSchema ) {
// allow changes to sub paths of mixed types
mixed = true ;
break ;
}
}
if ( schema == null ) {
// Check for embedded discriminators
schema = getEmbeddedDiscriminatorPath ( this , path ) ;
}
if ( ! mixed && ! schema ) {
if ( strict === 'throw' ) {
throw new StrictModeError ( path ) ;
}
return this ;
}
} else if ( pathType === 'virtual' ) {
schema = this . schema . virtualpath ( path ) ;
schema . applySetters ( val , this ) ;
return this ;
} else {
schema = this . $ _ _path ( path ) ;
}
// gh-4578, if setting a deeply nested path that doesn't exist yet, create it
let cur = this . _doc ;
let curPath = '' ;
for ( i = 0 ; i < parts . length - 1 ; ++ i ) {
cur = cur [ parts [ i ] ] ;
curPath += ( curPath . length > 0 ? '.' : '' ) + parts [ i ] ;
if ( ! cur ) {
this . $set ( curPath , { } ) ;
// Hack re: gh-5800. If nested field is not selected, it probably exists
// so `MongoError: cannot use the part (nested of nested.num) to
// traverse the element ({nested: null})` is not likely. If user gets
// that error, its their fault for now. We should reconsider disallowing
// modifying not selected paths for 6.x
if ( ! this . isSelected ( curPath ) ) {
this . unmarkModified ( curPath ) ;
}
2019-07-02 16:05:15 +02:00
cur = this . $ _ _getValue ( curPath ) ;
2019-02-01 14:06:44 +01:00
}
}
let pathToMark ;
// When using the $set operator the path to the field must already exist.
// Else mongodb throws: "LEFT_SUBFIELD only supports Object"
if ( parts . length <= 1 ) {
pathToMark = path ;
} else {
for ( i = 0 ; i < parts . length ; ++ i ) {
const subpath = parts . slice ( 0 , i + 1 ) . join ( '.' ) ;
if ( this . get ( subpath ) === null ) {
pathToMark = subpath ;
break ;
}
}
if ( ! pathToMark ) {
pathToMark = path ;
}
}
// if this doc is being constructed we should not trigger getters
const priorVal = ( ( ) => {
if ( this . $ _ _ . $options . priorDoc != null ) {
2019-07-02 16:05:15 +02:00
return this . $ _ _ . $options . priorDoc . $ _ _getValue ( path ) ;
2019-02-01 14:06:44 +01:00
}
if ( constructing ) {
return void 0 ;
}
2019-07-02 16:05:15 +02:00
return this . $ _ _getValue ( path ) ;
2019-02-01 14:06:44 +01:00
} ) ( ) ;
if ( ! schema ) {
this . $ _ _set ( pathToMark , path , constructing , parts , schema , val , priorVal ) ;
return this ;
}
let shouldSet = true ;
try {
// If the user is trying to set a ref path to a document with
// the correct model name, treat it as populated
const refMatches = ( ( ) => {
if ( schema . options == null ) {
return false ;
}
if ( ! ( val instanceof Document ) ) {
return false ;
}
const model = val . constructor ;
// Check ref
const ref = schema . options . ref ;
if ( ref != null && ( ref === model . modelName || ref === model . baseModelName ) ) {
return true ;
}
// Check refPath
const refPath = schema . options . refPath ;
if ( refPath == null ) {
return false ;
}
const modelName = val . get ( refPath ) ;
if ( modelName === model . modelName || modelName === model . baseModelName ) {
return true ;
}
return false ;
} ) ( ) ;
let didPopulate = false ;
if ( refMatches && val instanceof Document ) {
if ( this . ownerDocument ) {
this . ownerDocument ( ) . populated ( this . $ _ _fullPath ( path ) ,
2019-07-02 16:05:15 +02:00
val . _id , { [ populateModelSymbol ] : val . constructor } ) ;
2019-02-01 14:06:44 +01:00
} else {
2019-07-02 16:05:15 +02:00
this . populated ( path , val . _id , { [ populateModelSymbol ] : val . constructor } ) ;
2019-02-01 14:06:44 +01:00
}
didPopulate = true ;
}
let popOpts ;
if ( schema . options &&
Array . isArray ( schema . options [ this . schema . options . typeKey ] ) &&
schema . options [ this . schema . options . typeKey ] . length &&
schema . options [ this . schema . options . typeKey ] [ 0 ] . ref &&
Array . isArray ( val ) &&
val . length > 0 &&
val [ 0 ] instanceof Document &&
val [ 0 ] . constructor . modelName &&
( schema . options [ this . schema . options . typeKey ] [ 0 ] . ref === val [ 0 ] . constructor . baseModelName || schema . options [ this . schema . options . typeKey ] [ 0 ] . ref === val [ 0 ] . constructor . modelName ) ) {
if ( this . ownerDocument ) {
2019-07-02 16:05:15 +02:00
popOpts = { [ populateModelSymbol ] : val [ 0 ] . constructor } ;
2019-02-01 14:06:44 +01:00
this . ownerDocument ( ) . populated ( this . $ _ _fullPath ( path ) ,
val . map ( function ( v ) { return v . _id ; } ) , popOpts ) ;
} else {
2019-07-02 16:05:15 +02:00
popOpts = { [ populateModelSymbol ] : val [ 0 ] . constructor } ;
2019-02-01 14:06:44 +01:00
this . populated ( path , val . map ( function ( v ) { return v . _id ; } ) , popOpts ) ;
}
didPopulate = true ;
}
// If this path is underneath a single nested schema, we'll call the setter
// later in `$__set()` because we don't take `_doc` when we iterate through
// a single nested doc. That's to make sure we get the correct context.
// Otherwise we would double-call the setter, see gh-7196.
if ( this . schema . singleNestedPaths [ path ] == null ) {
2019-06-04 14:29:48 +02:00
val = schema . applySetters ( val , this , false , priorVal ) ;
2019-02-01 14:06:44 +01:00
}
if ( ! didPopulate && this . $ _ _ . populated ) {
delete this . $ _ _ . populated [ path ] ;
}
this . $markValid ( path ) ;
} catch ( e ) {
this . invalidate ( path ,
new MongooseError . CastError ( schema . instance , val , path , e ) ) ;
shouldSet = false ;
}
if ( shouldSet ) {
this . $ _ _set ( pathToMark , path , constructing , parts , schema , val , priorVal ) ;
}
if ( schema . $isSingleNested && ( this . isDirectModified ( path ) || val == null ) ) {
cleanModifiedSubpaths ( this , path ) ;
}
return this ;
} ;
/ * *
* Sets the value of a path , or many paths .
*
* # # # # Example :
*
* // path, value
* doc . set ( path , value )
*
* // object
* doc . set ( {
* path : value
* , path2 : {
* path : value
* }
* } )
*
* // on-the-fly cast to number
* doc . set ( path , value , Number )
*
* // on-the-fly cast to string
* doc . set ( path , value , String )
*
* // changing strict mode behavior
* doc . set ( path , value , { strict : false } ) ;
*
* @ param { String | Object } path path or object of key / vals to set
* @ param { Any } val the value to set
* @ param { Schema | String | Number | Buffer | * } [ type ] optionally specify a type for "on-the-fly" attributes
* @ param { Object } [ options ] optionally specify options that modify the behavior of the set
* @ api public
* @ method set
* @ memberOf Document
* @ instance
* /
Document . prototype . set = Document . prototype . $set ;
/ * *
* Determine if we should mark this change as modified .
*
* @ return { Boolean }
* @ api private
* @ method $ _ _shouldModify
* @ memberOf Document
* @ instance
* /
Document . prototype . $ _ _shouldModify = function ( pathToMark , path , constructing , parts , schema , val , priorVal ) {
if ( this . isNew ) {
return true ;
}
// Re: the note about gh-7196, `val` is the raw value without casting or
// setters if the full path is under a single nested subdoc because we don't
// want to double run setters. So don't set it as modified. See gh-7264.
if ( this . schema . singleNestedPaths [ path ] != null ) {
return false ;
}
if ( val === void 0 && ! this . isSelected ( path ) ) {
// when a path is not selected in a query, its initial
// value will be undefined.
return true ;
}
if ( val === void 0 && path in this . $ _ _ . activePaths . states . default ) {
// we're just unsetting the default value which was never saved
return false ;
}
// gh-3992: if setting a populated field to a doc, don't mark modified
// if they have the same _id
if ( this . populated ( path ) &&
val instanceof Document &&
deepEqual ( val . _id , priorVal ) ) {
return false ;
}
if ( ! deepEqual ( val , priorVal || this . get ( path ) ) ) {
return true ;
}
if ( ! constructing &&
val !== null &&
val !== undefined &&
path in this . $ _ _ . activePaths . states . default &&
deepEqual ( val , schema . getDefault ( this , constructing ) ) ) {
// a path with a default was $unset on the server
// and the user is setting it to the same value again
return true ;
}
return false ;
} ;
/ * *
* Handles the actual setting of the value and marking the path modified if appropriate .
*
* @ api private
* @ method $ _ _set
* @ memberOf Document
* @ instance
* /
Document . prototype . $ _ _set = function ( pathToMark , path , constructing , parts , schema , val , priorVal ) {
Embedded = Embedded || require ( './types/embedded' ) ;
const shouldModify = this . $ _ _shouldModify ( pathToMark , path , constructing , parts ,
schema , val , priorVal ) ;
const _this = this ;
if ( shouldModify ) {
this . markModified ( pathToMark ) ;
// handle directly setting arrays (gh-1126)
MongooseArray || ( MongooseArray = require ( './types/array' ) ) ;
if ( val && val . isMongooseArray ) {
val . _registerAtomic ( '$set' , val ) ;
// Update embedded document parent references (gh-5189)
if ( val . isMongooseDocumentArray ) {
val . forEach ( function ( item ) {
item && item . _ _parentArray && ( item . _ _parentArray = val ) ;
} ) ;
}
// Small hack for gh-1638: if we're overwriting the entire array, ignore
// paths that were modified before the array overwrite
this . $ _ _ . activePaths . forEach ( function ( modifiedPath ) {
2019-07-02 16:05:15 +02:00
if ( modifiedPath . startsWith ( path + '.' ) ) {
2019-02-01 14:06:44 +01:00
_this . $ _ _ . activePaths . ignore ( modifiedPath ) ;
}
} ) ;
}
}
let obj = this . _doc ;
let i = 0 ;
const l = parts . length ;
let cur = '' ;
for ( ; i < l ; i ++ ) {
const next = i + 1 ;
const last = next === l ;
cur += ( cur ? '.' + parts [ i ] : parts [ i ] ) ;
if ( specialProperties . has ( parts [ i ] ) ) {
return ;
}
if ( last ) {
if ( obj instanceof Map ) {
obj . set ( parts [ i ] , val ) ;
} else {
obj [ parts [ i ] ] = val ;
}
} else {
2019-06-04 14:29:48 +02:00
if ( utils . isPOJO ( obj [ parts [ i ] ] ) ) {
2019-02-01 14:06:44 +01:00
obj = obj [ parts [ i ] ] ;
} else if ( obj [ parts [ i ] ] && obj [ parts [ i ] ] instanceof Embedded ) {
obj = obj [ parts [ i ] ] ;
} else if ( obj [ parts [ i ] ] && obj [ parts [ i ] ] . $isSingleNested ) {
obj = obj [ parts [ i ] ] ;
} else if ( obj [ parts [ i ] ] && Array . isArray ( obj [ parts [ i ] ] ) ) {
obj = obj [ parts [ i ] ] ;
} else {
obj [ parts [ i ] ] = obj [ parts [ i ] ] || { } ;
obj = obj [ parts [ i ] ] ;
}
}
}
} ;
/ * *
* Gets a raw value from a path ( no getters )
*
* @ param { String } path
* @ api private
* /
2019-07-02 16:05:15 +02:00
Document . prototype . $ _ _getValue = function ( path ) {
2019-02-01 14:06:44 +01:00
return utils . getValue ( path , this . _doc ) ;
} ;
/ * *
* Sets a raw value for a path ( no casting , setters , transformations )
*
* @ param { String } path
* @ param { Object } value
* @ api private
* /
2019-07-02 16:05:15 +02:00
Document . prototype . $ _ _setValue = function ( path , val ) {
2019-02-01 14:06:44 +01:00
utils . setValue ( path , val , this . _doc ) ;
return this ;
} ;
/ * *
* Returns the value of a path .
*
* # # # # Example
*
* // path
* doc . get ( 'age' ) // 47
*
* // dynamic casting to a string
* doc . get ( 'age' , String ) // "47"
*
* @ param { String } path
* @ param { Schema | String | Number | Buffer | * } [ type ] optionally specify a type for on - the - fly attributes
2019-06-04 14:29:48 +02:00
* @ param { Object } [ options ]
* @ param { Boolean } [ options . virtuals = false ] Apply virtuals before getting this path
* @ param { Boolean } [ options . getters = true ] If false , skip applying getters and just get the raw value
2019-02-01 14:06:44 +01:00
* @ api public
* /
Document . prototype . get = function ( path , type , options ) {
let adhoc ;
options = options || { } ;
if ( type ) {
adhoc = this . schema . interpretAsType ( path , type , this . schema . options ) ;
}
const schema = this . $ _ _path ( path ) || this . schema . virtualpath ( path ) ;
const pieces = path . split ( '.' ) ;
let obj = this . _doc ;
if ( schema instanceof VirtualType ) {
if ( schema . getters . length === 0 ) {
return void 0 ;
}
return schema . applyGetters ( null , this ) ;
}
2019-06-04 14:29:48 +02:00
// Might need to change path for top-level alias
if ( typeof this . schema . aliases [ pieces [ 0 ] ] == 'string' ) {
pieces [ 0 ] = this . schema . aliases [ pieces [ 0 ] ] ;
}
2019-02-01 14:06:44 +01:00
for ( let i = 0 , l = pieces . length ; i < l ; i ++ ) {
if ( obj && obj . _doc ) {
obj = obj . _doc ;
}
if ( obj == null ) {
obj = void 0 ;
} else if ( obj instanceof Map ) {
obj = obj . get ( pieces [ i ] ) ;
} else if ( i === l - 1 ) {
obj = utils . getValue ( pieces [ i ] , obj ) ;
} else {
obj = obj [ pieces [ i ] ] ;
}
}
if ( adhoc ) {
obj = adhoc . cast ( obj ) ;
}
2019-06-04 14:29:48 +02:00
if ( schema != null && options . getters !== false ) {
2019-02-01 14:06:44 +01:00
obj = schema . applyGetters ( obj , this ) ;
} else if ( this . schema . nested [ path ] && options . virtuals ) {
// Might need to apply virtuals if this is a nested path
2019-06-04 14:29:48 +02:00
return applyGetters ( this , utils . clone ( obj ) || { } , 'virtuals' , { path : path } ) ;
2019-02-01 14:06:44 +01:00
}
return obj ;
} ;
/ * !
* ignore
* /
Document . prototype [ getSymbol ] = Document . prototype . get ;
/ * *
* Returns the schematype for the given ` path ` .
*
* @ param { String } path
* @ api private
* @ method $ _ _path
* @ memberOf Document
* @ instance
* /
Document . prototype . $ _ _path = function ( path ) {
const adhocs = this . $ _ _ . adhocPaths ;
const adhocType = adhocs && adhocs . hasOwnProperty ( path ) ? adhocs [ path ] : null ;
if ( adhocType ) {
return adhocType ;
}
return this . schema . path ( path ) ;
} ;
/ * *
* Marks the path as having pending changes to write to the db .
*
* _Very helpful when using [ Mixed ] ( . / schematypes . html # mixed ) types . _
*
* # # # # Example :
*
* doc . mixed . type = 'changed' ;
* doc . markModified ( 'mixed.type' ) ;
* doc . save ( ) // changes to mixed.type are now persisted
*
* @ param { String } path the path to mark modified
* @ param { Document } [ scope ] the scope to run validators with
* @ api public
* /
Document . prototype . markModified = function ( path , scope ) {
this . $ _ _ . activePaths . modify ( path ) ;
if ( scope != null && ! this . ownerDocument ) {
this . $ _ _ . pathsToScopes [ path ] = scope ;
}
} ;
/ * *
* Clears the modified state on the specified path .
*
* # # # # Example :
*
* doc . foo = 'bar' ;
* doc . unmarkModified ( 'foo' ) ;
* doc . save ( ) ; // changes to foo will not be persisted
*
* @ param { String } path the path to unmark modified
* @ api public
* /
Document . prototype . unmarkModified = function ( path ) {
this . $ _ _ . activePaths . init ( path ) ;
delete this . $ _ _ . pathsToScopes [ path ] ;
} ;
/ * *
* Don ' t run validation on this path or persist changes to this path .
*
* # # # # Example :
*
* doc . foo = null ;
* doc . $ignore ( 'foo' ) ;
* doc . save ( ) ; // changes to foo will not be persisted and validators won't be run
*
* @ memberOf Document
* @ instance
* @ method $ignore
* @ param { String } path the path to ignore
* @ api public
* /
Document . prototype . $ignore = function ( path ) {
this . $ _ _ . activePaths . ignore ( path ) ;
} ;
2019-06-04 14:29:48 +02:00
/ * *
* Returns the list of paths that have been directly modified . A direct
* modified path is a path that you explicitly set , whether via ` doc.foo = 'bar' ` ,
* ` Object.assign(doc, { foo: 'bar' }) ` , or ` doc.set('foo', 'bar') ` .
*
* A path ` a ` may be in ` modifiedPaths() ` but not in ` directModifiedPaths() `
* because a child of ` a ` was directly modified .
*
* # # # # Example
* const schema = new Schema ( { foo : String , nested : { bar : String } } ) ;
* const Model = mongoose . model ( 'Test' , schema ) ;
* await Model . create ( { foo : 'original' , nested : { bar : 'original' } } ) ;
*
* const doc = await Model . findOne ( ) ;
* doc . nested . bar = 'modified' ;
* doc . directModifiedPaths ( ) ; // ['nested.bar']
* doc . modifiedPaths ( ) ; // ['nested', 'nested.bar']
*
* @ return { Array }
* @ api public
* /
Document . prototype . directModifiedPaths = function ( ) {
return Object . keys ( this . $ _ _ . activePaths . states . modify ) ;
} ;
/ * *
* Returns true if the given path is nullish or only contains empty objects .
* Useful for determining whether this subdoc will get stripped out by the
* [ minimize option ] ( / d o c s / g u i d e . h t m l # m i n i m i z e ) .
*
* # # # # Example :
* const schema = new Schema ( { nested : { foo : String } } ) ;
* const Model = mongoose . model ( 'Test' , schema ) ;
* const doc = new Model ( { } ) ;
* doc . $isEmpty ( 'nested' ) ; // true
* doc . nested . $isEmpty ( ) ; // true
*
* doc . nested . foo = 'bar' ;
* doc . $isEmpty ( 'nested' ) ; // false
* doc . nested . $isEmpty ( ) ; // false
*
* @ memberOf Document
* @ instance
* @ api public
* @ method $isEmpty
* @ return { Boolean }
* /
Document . prototype . $isEmpty = function ( path ) {
const isEmptyOptions = {
minimize : true ,
virtuals : false ,
getters : false ,
transform : false
} ;
if ( arguments . length > 0 ) {
const v = this . get ( path ) ;
if ( v == null ) {
return true ;
}
if ( typeof v !== 'object' ) {
return false ;
}
if ( utils . isPOJO ( v ) ) {
return _isEmpty ( v ) ;
}
return Object . keys ( v . toObject ( isEmptyOptions ) ) . length === 0 ;
}
return Object . keys ( this . toObject ( isEmptyOptions ) ) . length === 0 ;
} ;
function _isEmpty ( v ) {
if ( v == null ) {
return true ;
}
if ( typeof v !== 'object' || Array . isArray ( v ) ) {
return false ;
}
for ( const key of Object . keys ( v ) ) {
if ( ! _isEmpty ( v [ key ] ) ) {
return false ;
}
}
return true ;
}
2019-02-01 14:06:44 +01:00
/ * *
* Returns the list of paths that have been modified .
*
* @ param { Object } [ options ]
* @ param { Boolean } [ options . includeChildren = false ] if true , returns children of modified paths as well . For example , if false , the list of modified paths for ` doc.colors = { primary: 'blue' }; ` will * * not * * contain ` colors.primary ` . If true , ` modifiedPaths() ` will return an array that contains ` colors.primary ` .
* @ return { Array }
* @ api public
* /
Document . prototype . modifiedPaths = function ( options ) {
options = options || { } ;
const directModifiedPaths = Object . keys ( this . $ _ _ . activePaths . states . modify ) ;
const _this = this ;
return directModifiedPaths . reduce ( function ( list , path ) {
const parts = path . split ( '.' ) ;
list = list . concat ( parts . reduce ( function ( chains , part , i ) {
return chains . concat ( parts . slice ( 0 , i ) . concat ( part ) . join ( '.' ) ) ;
} , [ ] ) . filter ( function ( chain ) {
return ( list . indexOf ( chain ) === - 1 ) ;
} ) ) ;
if ( ! options . includeChildren ) {
return list ;
}
let cur = _this . get ( path ) ;
if ( cur != null && typeof cur === 'object' ) {
if ( cur . _doc ) {
cur = cur . _doc ;
}
if ( Array . isArray ( cur ) ) {
const len = cur . length ;
for ( let i = 0 ; i < len ; ++ i ) {
if ( list . indexOf ( path + '.' + i ) === - 1 ) {
list . push ( path + '.' + i ) ;
if ( cur [ i ] != null && cur [ i ] . $ _ _ ) {
const modified = cur [ i ] . modifiedPaths ( ) ;
for ( const childPath of modified ) {
list . push ( path + '.' + i + '.' + childPath ) ;
}
}
}
}
} else {
Object . keys ( cur ) .
filter ( function ( key ) {
return list . indexOf ( path + '.' + key ) === - 1 ;
} ) .
forEach ( function ( key ) {
list . push ( path + '.' + key ) ;
} ) ;
}
}
return list ;
} , [ ] ) ;
} ;
/ * *
* Returns true if this document was modified , else false .
*
* If ` path ` is given , checks if a path or any full path containing ` path ` as part of its path chain has been modified .
*
* # # # # Example
*
* doc . set ( 'documents.0.title' , 'changed' ) ;
* doc . isModified ( ) // true
* doc . isModified ( 'documents' ) // true
* doc . isModified ( 'documents.0.title' ) // true
* doc . isModified ( 'documents otherProp' ) // true
* doc . isDirectModified ( 'documents' ) // false
*
* @ param { String } [ path ] optional
* @ return { Boolean }
* @ api public
* /
Document . prototype . isModified = function ( paths , modifiedPaths ) {
if ( paths ) {
if ( ! Array . isArray ( paths ) ) {
paths = paths . split ( ' ' ) ;
}
const modified = modifiedPaths || this . modifiedPaths ( ) ;
const directModifiedPaths = Object . keys ( this . $ _ _ . activePaths . states . modify ) ;
const isModifiedChild = paths . some ( function ( path ) {
return ! ! ~ modified . indexOf ( path ) ;
} ) ;
return isModifiedChild || paths . some ( function ( path ) {
return directModifiedPaths . some ( function ( mod ) {
2019-07-02 16:05:15 +02:00
return mod === path || path . startsWith ( mod + '.' ) ;
2019-02-01 14:06:44 +01:00
} ) ;
} ) ;
}
return this . $ _ _ . activePaths . some ( 'modify' ) ;
} ;
/ * *
* Checks if a path is set to its default .
*
* # # # # Example
*
* MyModel = mongoose . model ( 'test' , { name : { type : String , default : 'Val ' } } ) ;
* var m = new MyModel ( ) ;
* m . $isDefault ( 'name' ) ; // true
*
* @ memberOf Document
* @ instance
* @ method $isDefault
* @ param { String } [ path ]
* @ return { Boolean }
* @ api public
* /
Document . prototype . $isDefault = function ( path ) {
return ( path in this . $ _ _ . activePaths . states . default ) ;
} ;
/ * *
* Getter / setter , determines whether the document was removed or not .
*
* # # # # Example :
* product . remove ( function ( err , product ) {
2019-04-17 15:58:15 +02:00
* product . $isDeleted ( ) ; // true
2019-02-01 14:06:44 +01:00
* product . remove ( ) ; // no-op, doesn't send anything to the db
*
2019-04-17 15:58:15 +02:00
* product . $isDeleted ( false ) ;
* product . $isDeleted ( ) ; // false
2019-02-01 14:06:44 +01:00
* product . remove ( ) ; // will execute a remove against the db
* } )
*
* @ param { Boolean } [ val ] optional , overrides whether mongoose thinks the doc is deleted
* @ return { Boolean } whether mongoose thinks this doc is deleted .
* @ method $isDeleted
* @ memberOf Document
* @ instance
* @ api public
* /
Document . prototype . $isDeleted = function ( val ) {
if ( arguments . length === 0 ) {
return ! ! this . $ _ _ . isDeleted ;
}
this . $ _ _ . isDeleted = ! ! val ;
return this ;
} ;
/ * *
* Returns true if ` path ` was directly set and modified , else false .
*
* # # # # Example
*
* doc . set ( 'documents.0.title' , 'changed' ) ;
* doc . isDirectModified ( 'documents.0.title' ) // true
* doc . isDirectModified ( 'documents' ) // false
*
* @ param { String } path
* @ return { Boolean }
* @ api public
* /
Document . prototype . isDirectModified = function ( path ) {
return ( path in this . $ _ _ . activePaths . states . modify ) ;
} ;
/ * *
* Checks if ` path ` was initialized .
*
* @ param { String } path
* @ return { Boolean }
* @ api public
* /
Document . prototype . isInit = function ( path ) {
return ( path in this . $ _ _ . activePaths . states . init ) ;
} ;
/ * *
* Checks if ` path ` was selected in the source query which initialized this document .
*
* # # # # Example
*
* Thing . findOne ( ) . select ( 'name' ) . exec ( function ( err , doc ) {
* doc . isSelected ( 'name' ) // true
* doc . isSelected ( 'age' ) // false
* } )
*
* @ param { String } path
* @ return { Boolean }
* @ api public
* /
Document . prototype . isSelected = function isSelected ( path ) {
if ( this . $ _ _ . selected ) {
if ( path === '_id' ) {
return this . $ _ _ . selected . _id !== 0 ;
}
const paths = Object . keys ( this . $ _ _ . selected ) ;
let i = paths . length ;
let inclusive = null ;
let cur ;
if ( i === 1 && paths [ 0 ] === '_id' ) {
// only _id was selected.
return this . $ _ _ . selected . _id === 0 ;
}
while ( i -- ) {
cur = paths [ i ] ;
if ( cur === '_id' ) {
continue ;
}
if ( ! isDefiningProjection ( this . $ _ _ . selected [ cur ] ) ) {
continue ;
}
inclusive = ! ! this . $ _ _ . selected [ cur ] ;
break ;
}
if ( inclusive === null ) {
return true ;
}
if ( path in this . $ _ _ . selected ) {
return inclusive ;
}
i = paths . length ;
const pathDot = path + '.' ;
while ( i -- ) {
cur = paths [ i ] ;
if ( cur === '_id' ) {
continue ;
}
2019-07-02 16:05:15 +02:00
if ( cur . startsWith ( pathDot ) ) {
2019-02-01 14:06:44 +01:00
return inclusive || cur !== pathDot ;
}
2019-07-02 16:05:15 +02:00
if ( pathDot . startsWith ( cur + '.' ) ) {
2019-02-01 14:06:44 +01:00
return inclusive ;
}
}
return ! inclusive ;
}
return true ;
} ;
/ * *
* Checks if ` path ` was explicitly selected . If no projection , always returns
* true .
*
* # # # # Example
*
* Thing . findOne ( ) . select ( 'nested.name' ) . exec ( function ( err , doc ) {
* doc . isDirectSelected ( 'nested.name' ) // true
* doc . isDirectSelected ( 'nested.otherName' ) // false
* doc . isDirectSelected ( 'nested' ) // false
* } )
*
* @ param { String } path
* @ return { Boolean }
* @ api public
* /
Document . prototype . isDirectSelected = function isDirectSelected ( path ) {
if ( this . $ _ _ . selected ) {
if ( path === '_id' ) {
return this . $ _ _ . selected . _id !== 0 ;
}
const paths = Object . keys ( this . $ _ _ . selected ) ;
let i = paths . length ;
let inclusive = null ;
let cur ;
if ( i === 1 && paths [ 0 ] === '_id' ) {
// only _id was selected.
return this . $ _ _ . selected . _id === 0 ;
}
while ( i -- ) {
cur = paths [ i ] ;
if ( cur === '_id' ) {
continue ;
}
if ( ! isDefiningProjection ( this . $ _ _ . selected [ cur ] ) ) {
continue ;
}
inclusive = ! ! this . $ _ _ . selected [ cur ] ;
break ;
}
if ( inclusive === null ) {
return true ;
}
if ( path in this . $ _ _ . selected ) {
return inclusive ;
}
return ! inclusive ;
}
return true ;
} ;
/ * *
* Executes registered validation rules for this document .
*
* # # # # Note :
*
* This method is called ` pre ` save and if a validation rule is violated , [ save ] ( # model _Model - save ) is aborted and the error is returned to your ` callback ` .
*
* # # # # Example :
*
* doc . validate ( function ( err ) {
* if ( err ) handleError ( err ) ;
* else // validation passed
* } ) ;
*
* @ param { Object } optional options internal options
* @ param { Function } callback optional callback called after validation completes , passing an error if one occurred
* @ return { Promise } Promise
* @ api public
* /
Document . prototype . validate = function ( options , callback ) {
if ( typeof options === 'function' ) {
callback = options ;
options = null ;
}
2019-06-04 14:29:48 +02:00
return utils . promiseOrCallback ( callback , cb => this . $ _ _validate ( options , function ( error ) {
2019-02-01 14:06:44 +01:00
cb ( error ) ;
} ) , this . constructor . events ) ;
} ;
/ * !
* ignore
* /
function _evaluateRequiredFunctions ( doc ) {
Object . keys ( doc . $ _ _ . activePaths . states . require ) . forEach ( path => {
const p = doc . schema . path ( path ) ;
if ( p != null && typeof p . originalRequiredValue === 'function' ) {
doc . $ _ _ . cachedRequired [ path ] = p . originalRequiredValue . call ( doc ) ;
}
} ) ;
}
/ * !
* ignore
* /
function _getPathsToValidate ( doc ) {
let i ;
let len ;
const skipSchemaValidators = { } ;
_evaluateRequiredFunctions ( doc ) ;
// only validate required fields when necessary
let paths = Object . keys ( doc . $ _ _ . activePaths . states . require ) . filter ( function ( path ) {
if ( ! doc . isSelected ( path ) && ! doc . isModified ( path ) ) {
return false ;
}
if ( path in doc . $ _ _ . cachedRequired ) {
return doc . $ _ _ . cachedRequired [ path ] ;
}
return true ;
} ) ;
paths = paths . concat ( Object . keys ( doc . $ _ _ . activePaths . states . init ) ) ;
paths = paths . concat ( Object . keys ( doc . $ _ _ . activePaths . states . modify ) ) ;
paths = paths . concat ( Object . keys ( doc . $ _ _ . activePaths . states . default ) ) ;
if ( ! doc . ownerDocument ) {
const subdocs = doc . $ _ _getAllSubdocs ( ) ;
let subdoc ;
len = subdocs . length ;
const modifiedPaths = doc . modifiedPaths ( ) ;
for ( i = 0 ; i < len ; ++ i ) {
subdoc = subdocs [ i ] ;
if ( doc . isModified ( subdoc . $basePath , modifiedPaths ) &&
2019-06-04 14:29:48 +02:00
! doc . isDirectModified ( subdoc . $basePath ) &&
! doc . $isDefault ( subdoc . $basePath ) ) {
2019-02-01 14:06:44 +01:00
// Remove child paths for now, because we'll be validating the whole
// subdoc
paths = paths . filter ( function ( p ) {
return p != null && p . indexOf ( subdoc . $basePath + '.' ) !== 0 ;
} ) ;
paths . push ( subdoc . $basePath ) ;
skipSchemaValidators [ subdoc . $basePath ] = true ;
}
}
}
// gh-661: if a whole array is modified, make sure to run validation on all
// the children as well
len = paths . length ;
for ( i = 0 ; i < len ; ++ i ) {
const path = paths [ i ] ;
const _pathType = doc . schema . path ( path ) ;
if ( ! _pathType ||
! _pathType . $isMongooseArray ||
// To avoid potential performance issues, skip doc arrays whose children
// are not required. `getPositionalPathType()` may be slow, so avoid
// it unless we have a case of #6364
( _pathType . $isMongooseDocumentArray && ! get ( _pathType , 'schemaOptions.required' ) ) ) {
continue ;
}
2019-07-02 16:05:15 +02:00
const val = doc . $ _ _getValue ( path ) ;
2019-02-01 14:06:44 +01:00
if ( val ) {
const numElements = val . length ;
for ( let j = 0 ; j < numElements ; ++ j ) {
paths . push ( path + '.' + j ) ;
}
}
}
const flattenOptions = { skipArrays : true } ;
len = paths . length ;
for ( i = 0 ; i < len ; ++ i ) {
const pathToCheck = paths [ i ] ;
if ( doc . schema . nested [ pathToCheck ] ) {
2019-07-02 16:05:15 +02:00
let _v = doc . $ _ _getValue ( pathToCheck ) ;
2019-02-01 14:06:44 +01:00
if ( isMongooseObject ( _v ) ) {
_v = _v . toObject ( { transform : false } ) ;
}
const flat = flatten ( _v , '' , flattenOptions ) ;
const _subpaths = Object . keys ( flat ) . map ( function ( p ) {
return pathToCheck + '.' + p ;
} ) ;
paths = paths . concat ( _subpaths ) ;
}
}
len = paths . length ;
for ( i = 0 ; i < len ; ++ i ) {
const path = paths [ i ] ;
const _pathType = doc . schema . path ( path ) ;
if ( ! _pathType || ! _pathType . $isSchemaMap ) {
continue ;
}
2019-07-02 16:05:15 +02:00
const val = doc . $ _ _getValue ( path ) ;
2019-02-01 14:06:44 +01:00
if ( val == null ) {
continue ;
}
for ( const key of val . keys ( ) ) {
paths . push ( path + '.' + key ) ;
}
}
return [ paths , skipSchemaValidators ] ;
}
/ * !
* ignore
* /
2019-06-04 14:29:48 +02:00
Document . prototype . $ _ _validate = function ( options , callback ) {
if ( typeof options === 'function' ) {
callback = options ;
options = null ;
}
const hasValidateModifiedOnlyOption = options &&
( typeof options === 'object' ) &&
( 'validateModifiedOnly' in options ) ;
let shouldValidateModifiedOnly ;
if ( hasValidateModifiedOnlyOption ) {
shouldValidateModifiedOnly = ! ! options . validateModifiedOnly ;
} else {
shouldValidateModifiedOnly = this . schema . options . validateModifiedOnly ;
}
2019-02-01 14:06:44 +01:00
const _this = this ;
const _complete = ( ) => {
const err = this . $ _ _ . validationError ;
this . $ _ _ . validationError = undefined ;
this . $ _ _ . cachedRequired = { } ;
this . emit ( 'validate' , _this ) ;
this . constructor . emit ( 'validate' , _this ) ;
if ( err ) {
for ( const key in err . errors ) {
// Make sure cast errors persist
if ( ! this [ documentArrayParent ] && err . errors [ key ] instanceof MongooseError . CastError ) {
this . invalidate ( key , err . errors [ key ] ) ;
}
}
return err ;
}
} ;
// only validate required fields when necessary
const pathDetails = _getPathsToValidate ( this ) ;
2019-06-04 14:29:48 +02:00
const paths = shouldValidateModifiedOnly ?
pathDetails [ 0 ] . filter ( ( path ) => this . isModified ( path ) ) :
pathDetails [ 0 ] ;
2019-02-01 14:06:44 +01:00
const skipSchemaValidators = pathDetails [ 1 ] ;
if ( paths . length === 0 ) {
return process . nextTick ( function ( ) {
const error = _complete ( ) ;
if ( error ) {
return _this . schema . s . hooks . execPost ( 'validate:error' , _this , [ _this ] , { error : error } , function ( error ) {
callback ( error ) ;
} ) ;
}
callback ( null , _this ) ;
} ) ;
}
const validated = { } ;
let total = 0 ;
const complete = function ( ) {
const error = _complete ( ) ;
if ( error ) {
return _this . schema . s . hooks . execPost ( 'validate:error' , _this , [ _this ] , { error : error } , function ( error ) {
callback ( error ) ;
} ) ;
}
callback ( null , _this ) ;
} ;
const validatePath = function ( path ) {
if ( path == null || validated [ path ] ) {
return ;
}
validated [ path ] = true ;
total ++ ;
process . nextTick ( function ( ) {
const p = _this . schema . path ( path ) ;
if ( ! p ) {
return -- total || complete ( ) ;
}
// If user marked as invalid or there was a cast error, don't validate
if ( ! _this . $isValid ( path ) ) {
-- total || complete ( ) ;
return ;
}
2019-07-02 16:05:15 +02:00
const val = _this . $ _ _getValue ( path ) ;
2019-02-01 14:06:44 +01:00
const scope = path in _this . $ _ _ . pathsToScopes ?
_this . $ _ _ . pathsToScopes [ path ] :
_this ;
p . doValidate ( val , function ( err ) {
if ( err && ( ! p . $isMongooseDocumentArray || err . $isArrayValidatorError ) ) {
if ( p . $isSingleNested &&
err . name === 'ValidationError' &&
p . schema . options . storeSubdocValidationError === false ) {
return -- total || complete ( ) ;
}
_this . invalidate ( path , err , undefined , true ) ;
}
-- total || complete ( ) ;
2019-06-04 14:29:48 +02:00
} , scope , { skipSchemaValidators : skipSchemaValidators [ path ] , path : path } ) ;
2019-02-01 14:06:44 +01:00
} ) ;
} ;
const numPaths = paths . length ;
for ( let i = 0 ; i < numPaths ; ++ i ) {
validatePath ( paths [ i ] ) ;
}
} ;
/ * *
* Executes registered validation rules ( skipping asynchronous validators ) for this document .
*
* # # # # Note :
*
* This method is useful if you need synchronous validation .
*
* # # # # Example :
*
* var err = doc . validateSync ( ) ;
* if ( err ) {
* handleError ( err ) ;
* } else {
* // validation passed
* }
*
* @ param { Array | string } pathsToValidate only validate the given paths
2019-06-04 14:29:48 +02:00
* @ return { ValidationError | undefined } ValidationError if there are errors during validation , or undefined if there is no error .
2019-02-01 14:06:44 +01:00
* @ api public
* /
2019-06-04 14:29:48 +02:00
Document . prototype . validateSync = function ( pathsToValidate , options ) {
2019-02-01 14:06:44 +01:00
const _this = this ;
2019-06-04 14:29:48 +02:00
const hasValidateModifiedOnlyOption = options &&
( typeof options === 'object' ) &&
( 'validateModifiedOnly' in options ) ;
let shouldValidateModifiedOnly ;
if ( hasValidateModifiedOnlyOption ) {
shouldValidateModifiedOnly = ! ! options . validateModifiedOnly ;
} else {
shouldValidateModifiedOnly = this . schema . options . validateModifiedOnly ;
}
2019-02-01 14:06:44 +01:00
if ( typeof pathsToValidate === 'string' ) {
pathsToValidate = pathsToValidate . split ( ' ' ) ;
}
// only validate required fields when necessary
const pathDetails = _getPathsToValidate ( this ) ;
2019-06-04 14:29:48 +02:00
let paths = shouldValidateModifiedOnly ?
pathDetails [ 0 ] . filter ( ( path ) => this . isModified ( path ) ) :
pathDetails [ 0 ] ;
2019-02-01 14:06:44 +01:00
const skipSchemaValidators = pathDetails [ 1 ] ;
if ( pathsToValidate && pathsToValidate . length ) {
const tmp = [ ] ;
for ( let i = 0 ; i < paths . length ; ++ i ) {
if ( pathsToValidate . indexOf ( paths [ i ] ) !== - 1 ) {
tmp . push ( paths [ i ] ) ;
}
}
paths = tmp ;
}
const validating = { } ;
paths . forEach ( function ( path ) {
if ( validating [ path ] ) {
return ;
}
validating [ path ] = true ;
const p = _this . schema . path ( path ) ;
if ( ! p ) {
return ;
}
if ( ! _this . $isValid ( path ) ) {
return ;
}
2019-07-02 16:05:15 +02:00
const val = _this . $ _ _getValue ( path ) ;
2019-02-01 14:06:44 +01:00
const err = p . doValidateSync ( val , _this , {
2019-06-04 14:29:48 +02:00
skipSchemaValidators : skipSchemaValidators [ path ] ,
path : path
2019-02-01 14:06:44 +01:00
} ) ;
if ( err && ( ! p . $isMongooseDocumentArray || err . $isArrayValidatorError ) ) {
if ( p . $isSingleNested &&
err . name === 'ValidationError' &&
p . schema . options . storeSubdocValidationError === false ) {
return ;
}
_this . invalidate ( path , err , undefined , true ) ;
}
} ) ;
const err = _this . $ _ _ . validationError ;
_this . $ _ _ . validationError = undefined ;
_this . emit ( 'validate' , _this ) ;
_this . constructor . emit ( 'validate' , _this ) ;
if ( err ) {
for ( const key in err . errors ) {
// Make sure cast errors persist
if ( err . errors [ key ] instanceof MongooseError . CastError ) {
_this . invalidate ( key , err . errors [ key ] ) ;
}
}
}
return err ;
} ;
/ * *
* Marks a path as invalid , causing validation to fail .
*
* The ` errorMsg ` argument will become the message of the ` ValidationError ` .
*
* The ` value ` argument ( if passed ) will be available through the ` ValidationError.value ` property .
*
* doc . invalidate ( 'size' , 'must be less than 20' , 14 ) ;
* doc . validate ( function ( err ) {
* console . log ( err )
* // prints
* { message : 'Validation failed' ,
* name : 'ValidationError' ,
* errors :
* { size :
* { message : 'must be less than 20' ,
* name : 'ValidatorError' ,
* path : 'size' ,
* type : 'user defined' ,
* value : 14 } } }
* } )
*
* @ param { String } path the field to invalidate
* @ param { String | Error } errorMsg the error which states the reason ` path ` was invalid
* @ param { Object | String | Number | any } value optional invalid value
* @ param { String } [ kind ] optional ` kind ` property for the error
* @ return { ValidationError } the current ValidationError , with all currently invalidated paths
* @ api public
* /
Document . prototype . invalidate = function ( path , err , val , kind ) {
if ( ! this . $ _ _ . validationError ) {
this . $ _ _ . validationError = new ValidationError ( this ) ;
}
if ( this . $ _ _ . validationError . errors [ path ] ) {
return ;
}
if ( ! err || typeof err === 'string' ) {
err = new ValidatorError ( {
path : path ,
message : err ,
type : kind || 'user defined' ,
value : val
} ) ;
}
if ( this . $ _ _ . validationError === err ) {
return this . $ _ _ . validationError ;
}
this . $ _ _ . validationError . addError ( path , err ) ;
return this . $ _ _ . validationError ;
} ;
/ * *
* Marks a path as valid , removing existing validation errors .
*
* @ param { String } path the field to mark as valid
* @ api public
* @ memberOf Document
* @ instance
* @ method $markValid
* /
Document . prototype . $markValid = function ( path ) {
if ( ! this . $ _ _ . validationError || ! this . $ _ _ . validationError . errors [ path ] ) {
return ;
}
delete this . $ _ _ . validationError . errors [ path ] ;
if ( Object . keys ( this . $ _ _ . validationError . errors ) . length === 0 ) {
this . $ _ _ . validationError = null ;
}
} ;
/ * *
* Saves this document .
*
* # # # # Example :
*
* product . sold = Date . now ( ) ;
* product . save ( function ( err , product ) {
* if ( err ) . .
* } )
*
* The callback will receive two parameters
*
* 1. ` err ` if an error occurred
* 2. ` product ` which is the saved ` product `
*
* As an extra measure of flow control , save will return a Promise .
* # # # # Example :
* product . save ( ) . then ( function ( product ) {
* ...
* } ) ;
*
* @ param { Object } [ options ] options optional options
* @ param { Object } [ options . safe ] ( DEPRECATED ) overrides [ schema ' s safe option ] ( http : //mongoosejs.com//docs/guide.html#safe)
* @ param { Boolean } [ options . validateBeforeSave ] set to false to save without validating .
* @ param { Function } [ fn ] optional callback
* @ method save
* @ memberOf Document
* @ instance
* @ return { Promise | undefined } Returns undefined if used with callback or a Promise otherwise .
* @ api public
* @ see middleware http : //mongoosejs.com/docs/middleware.html
* /
/ * *
* Checks if a path is invalid
*
* @ param { String } path the field to check
* @ method $isValid
* @ memberOf Document
* @ instance
* @ api private
* /
Document . prototype . $isValid = function ( path ) {
return ! this . $ _ _ . validationError || ! this . $ _ _ . validationError . errors [ path ] ;
} ;
/ * *
* Resets the internal modified state of this document .
*
* @ api private
* @ return { Document }
* @ method $ _ _reset
* @ memberOf Document
* @ instance
* /
Document . prototype . $ _ _reset = function reset ( ) {
let _this = this ;
DocumentArray || ( DocumentArray = require ( './types/documentarray' ) ) ;
this . $ _ _ . activePaths
. map ( 'init' , 'modify' , function ( i ) {
2019-07-02 16:05:15 +02:00
return _this . $ _ _getValue ( i ) ;
2019-02-01 14:06:44 +01:00
} )
. filter ( function ( val ) {
return val && val instanceof Array && val . isMongooseDocumentArray && val . length ;
} )
. forEach ( function ( array ) {
let i = array . length ;
while ( i -- ) {
const doc = array [ i ] ;
if ( ! doc ) {
continue ;
}
doc . $ _ _reset ( ) ;
}
2019-06-04 14:29:48 +02:00
_this . $ _ _ . activePaths . init ( array . $path ( ) ) ;
2019-02-01 14:06:44 +01:00
2019-06-04 14:29:48 +02:00
array [ arrayAtomicsSymbol ] = { } ;
2019-02-01 14:06:44 +01:00
} ) ;
this . $ _ _ . activePaths .
map ( 'init' , 'modify' , function ( i ) {
2019-07-02 16:05:15 +02:00
return _this . $ _ _getValue ( i ) ;
2019-02-01 14:06:44 +01:00
} ) .
filter ( function ( val ) {
return val && val . $isSingleNested ;
} ) .
forEach ( function ( doc ) {
doc . $ _ _reset ( ) ;
_this . $ _ _ . activePaths . init ( doc . $basePath ) ;
} ) ;
// clear atomics
this . $ _ _dirty ( ) . forEach ( function ( dirt ) {
const type = dirt . value ;
2019-06-04 14:29:48 +02:00
if ( type && type [ arrayAtomicsSymbol ] ) {
type [ arrayAtomicsSymbol ] = { } ;
2019-02-01 14:06:44 +01:00
}
} ) ;
// Clear 'dirty' cache
this . $ _ _ . activePaths . clear ( 'modify' ) ;
this . $ _ _ . activePaths . clear ( 'default' ) ;
this . $ _ _ . validationError = undefined ;
this . errors = undefined ;
_this = this ;
this . schema . requiredPaths ( ) . forEach ( function ( path ) {
_this . $ _ _ . activePaths . require ( path ) ;
} ) ;
return this ;
} ;
/ * *
* Returns this documents dirty paths / vals .
*
* @ api private
* @ method $ _ _dirty
* @ memberOf Document
* @ instance
* /
Document . prototype . $ _ _dirty = function ( ) {
const _this = this ;
let all = this . $ _ _ . activePaths . map ( 'modify' , function ( path ) {
return {
path : path ,
2019-07-02 16:05:15 +02:00
value : _this . $ _ _getValue ( path ) ,
2019-02-01 14:06:44 +01:00
schema : _this . $ _ _path ( path )
} ;
} ) ;
// gh-2558: if we had to set a default and the value is not undefined,
// we have to save as well
all = all . concat ( this . $ _ _ . activePaths . map ( 'default' , function ( path ) {
2019-07-02 16:05:15 +02:00
if ( path === '_id' || _this . $ _ _getValue ( path ) == null ) {
2019-02-01 14:06:44 +01:00
return ;
}
return {
path : path ,
2019-07-02 16:05:15 +02:00
value : _this . $ _ _getValue ( path ) ,
2019-02-01 14:06:44 +01:00
schema : _this . $ _ _path ( path )
} ;
} ) ) ;
// Sort dirty paths in a flat hierarchy.
all . sort ( function ( a , b ) {
return ( a . path < b . path ? - 1 : ( a . path > b . path ? 1 : 0 ) ) ;
} ) ;
// Ignore "foo.a" if "foo" is dirty already.
const minimal = [ ] ;
let lastPath ;
let top ;
all . forEach ( function ( item ) {
if ( ! item ) {
return ;
}
2019-06-04 14:29:48 +02:00
if ( lastPath == null || item . path . indexOf ( lastPath ) !== 0 ) {
2019-02-01 14:06:44 +01:00
lastPath = item . path + '.' ;
minimal . push ( item ) ;
top = item ;
2019-06-04 14:29:48 +02:00
} else if ( top != null &&
top . value != null &&
top . value [ arrayAtomicsSymbol ] != null &&
top . value . hasAtomics ( ) ) {
2019-02-01 14:06:44 +01:00
// special case for top level MongooseArrays
2019-06-04 14:29:48 +02:00
// the `top` array itself and a sub path of `top` are being modified.
// the only way to honor all of both modifications is through a $set
// of entire array.
top . value [ arrayAtomicsSymbol ] = { } ;
top . value [ arrayAtomicsSymbol ] . $set = top . value ;
2019-02-01 14:06:44 +01:00
}
} ) ;
top = lastPath = null ;
return minimal ;
} ;
/ * *
* Assigns / compiles ` schema ` into this documents prototype .
*
* @ param { Schema } schema
* @ api private
* @ method $ _ _setSchema
* @ memberOf Document
* @ instance
* /
Document . prototype . $ _ _setSchema = function ( schema ) {
schema . plugin ( idGetter , { deduplicate : true } ) ;
compile ( schema . tree , this , undefined , schema . options ) ;
// Apply default getters if virtual doesn't have any (gh-6262)
for ( const key of Object . keys ( schema . virtuals ) ) {
schema . virtuals [ key ] . _applyDefaultGetters ( ) ;
}
this . schema = schema ;
2019-07-02 16:05:15 +02:00
this [ documentSchemaSymbol ] = schema ;
2019-02-01 14:06:44 +01:00
} ;
/ * *
* Get active path that were changed and are arrays
*
* @ api private
* @ method $ _ _getArrayPathsToValidate
* @ memberOf Document
* @ instance
* /
Document . prototype . $ _ _getArrayPathsToValidate = function ( ) {
DocumentArray || ( DocumentArray = require ( './types/documentarray' ) ) ;
// validate all document arrays.
return this . $ _ _ . activePaths
. map ( 'init' , 'modify' , function ( i ) {
2019-07-02 16:05:15 +02:00
return this . $ _ _getValue ( i ) ;
2019-02-01 14:06:44 +01:00
} . bind ( this ) )
. filter ( function ( val ) {
return val && val instanceof Array && val . isMongooseDocumentArray && val . length ;
} ) . reduce ( function ( seed , array ) {
return seed . concat ( array ) ;
} , [ ] )
. filter ( function ( doc ) {
return doc ;
} ) ;
} ;
/ * *
* Get all subdocs ( by bfs )
*
* @ api private
* @ method $ _ _getAllSubdocs
* @ memberOf Document
* @ instance
* /
Document . prototype . $ _ _getAllSubdocs = function ( ) {
DocumentArray || ( DocumentArray = require ( './types/documentarray' ) ) ;
Embedded = Embedded || require ( './types/embedded' ) ;
function docReducer ( doc , seed , path ) {
2019-06-04 14:29:48 +02:00
let val = doc ;
if ( path ) {
2019-07-02 16:05:15 +02:00
if ( doc instanceof Document && doc [ documentSchemaSymbol ] . paths [ path ] ) {
2019-06-04 14:29:48 +02:00
val = doc . _doc [ path ] ;
} else {
val = doc [ path ] ;
}
}
2019-02-01 14:06:44 +01:00
if ( val instanceof Embedded ) {
seed . push ( val ) ;
2019-06-04 14:29:48 +02:00
} else if ( val instanceof Map ) {
2019-02-01 14:06:44 +01:00
seed = Array . from ( val . keys ( ) ) . reduce ( function ( seed , path ) {
return docReducer ( val . get ( path ) , seed , null ) ;
} , seed ) ;
2019-06-04 14:29:48 +02:00
} else if ( val && val . $isSingleNested ) {
2019-02-01 14:06:44 +01:00
seed = Object . keys ( val . _doc ) . reduce ( function ( seed , path ) {
return docReducer ( val . _doc , seed , path ) ;
} , seed ) ;
seed . push ( val ) ;
2019-06-04 14:29:48 +02:00
} else if ( val && val . isMongooseDocumentArray ) {
2019-02-01 14:06:44 +01:00
val . forEach ( function _docReduce ( doc ) {
if ( ! doc || ! doc . _doc ) {
return ;
}
if ( doc instanceof Embedded ) {
seed . push ( doc ) ;
}
seed = Object . keys ( doc . _doc ) . reduce ( function ( seed , path ) {
return docReducer ( doc . _doc , seed , path ) ;
} , seed ) ;
} ) ;
} else if ( val instanceof Document && val . $ _ _isNested ) {
2019-06-04 14:29:48 +02:00
seed = Object . keys ( val ) . reduce ( function ( seed , path ) {
return docReducer ( val , seed , path ) ;
} , seed ) ;
2019-02-01 14:06:44 +01:00
}
return seed ;
}
const _this = this ;
const subDocs = Object . keys ( this . _doc ) . reduce ( function ( seed , path ) {
return docReducer ( _this , seed , path ) ;
} , [ ] ) ;
return subDocs ;
} ;
/ * !
* Runs queued functions
* /
function applyQueue ( doc ) {
const q = doc . schema && doc . schema . callQueue ;
if ( ! q . length ) {
return ;
}
let pair ;
for ( let i = 0 ; i < q . length ; ++ i ) {
pair = q [ i ] ;
if ( pair [ 0 ] !== 'pre' && pair [ 0 ] !== 'post' && pair [ 0 ] !== 'on' ) {
doc [ pair [ 0 ] ] . apply ( doc , pair [ 1 ] ) ;
}
}
}
/ * !
* ignore
* /
Document . prototype . $ _ _handleReject = function handleReject ( err ) {
// emit on the Model if listening
if ( this . listeners ( 'error' ) . length ) {
this . emit ( 'error' , err ) ;
} else if ( this . constructor . listeners && this . constructor . listeners ( 'error' ) . length ) {
this . constructor . emit ( 'error' , err ) ;
} else if ( this . listeners && this . listeners ( 'error' ) . length ) {
this . emit ( 'error' , err ) ;
}
} ;
/ * *
* Internal helper for toObject ( ) and toJSON ( ) that doesn ' t manipulate options
*
* @ api private
* @ method $toObject
* @ memberOf Document
* @ instance
* /
Document . prototype . $toObject = function ( options , json ) {
let defaultOptions = {
transform : true ,
flattenDecimals : true
} ;
const path = json ? 'toJSON' : 'toObject' ;
const baseOptions = get ( this , 'constructor.base.options.' + path , { } ) ;
const schemaOptions = get ( this , 'schema.options' , { } ) ;
// merge base default options with Schema's set default options if available.
// `clone` is necessary here because `utils.options` directly modifies the second input.
defaultOptions = utils . options ( defaultOptions , clone ( baseOptions ) ) ;
defaultOptions = utils . options ( defaultOptions , clone ( schemaOptions [ path ] || { } ) ) ;
// If options do not exist or is not an object, set it to empty object
2019-06-04 14:29:48 +02:00
options = utils . isPOJO ( options ) ? clone ( options ) : { } ;
2019-02-01 14:06:44 +01:00
if ( ! ( 'flattenMaps' in options ) ) {
options . flattenMaps = defaultOptions . flattenMaps ;
}
let _minimize ;
if ( options . minimize != null ) {
_minimize = options . minimize ;
} else if ( defaultOptions . minimize != null ) {
_minimize = defaultOptions . minimize ;
} else {
_minimize = schemaOptions . minimize ;
}
// The original options that will be passed to `clone()`. Important because
// `clone()` will recursively call `$toObject()` on embedded docs, so we
// need the original options the user passed in, plus `_isNested` and
// `_parentOptions` for checking whether we need to depopulate.
const cloneOptions = Object . assign ( utils . clone ( options ) , {
_isNested : true ,
json : json ,
minimize : _minimize
} ) ;
const depopulate = options . depopulate ||
get ( options , '_parentOptions.depopulate' , false ) ;
// _isNested will only be true if this is not the top level document, we
// should never depopulate
if ( depopulate && options . _isNested && this . $ _ _ . wasPopulated ) {
// populated paths that we set to a document
return clone ( this . _id , cloneOptions ) ;
}
// merge default options with input options.
options = utils . options ( defaultOptions , options ) ;
options . _isNested = true ;
options . json = json ;
options . minimize = _minimize ;
cloneOptions . _parentOptions = options ;
// remember the root transform function
// to save it from being overwritten by sub-transform functions
const originalTransform = options . transform ;
let ret = clone ( this . _doc , cloneOptions ) || { } ;
if ( options . getters ) {
applyGetters ( this , ret , 'paths' , cloneOptions ) ;
// applyGetters for paths will add nested empty objects;
// if minimize is set, we need to remove them.
if ( options . minimize ) {
ret = minimize ( ret ) || { } ;
}
}
if ( options . virtuals || options . getters && options . virtuals !== false ) {
applyGetters ( this , ret , 'virtuals' , cloneOptions ) ;
}
if ( options . versionKey === false && this . schema . options . versionKey ) {
delete ret [ this . schema . options . versionKey ] ;
}
let transform = options . transform ;
// In the case where a subdocument has its own transform function, we need to
// check and see if the parent has a transform (options.transform) and if the
// child schema has a transform (this.schema.options.toObject) In this case,
// we need to adjust options.transform to be the child schema's transform and
// not the parent schema's
if ( transform === true || ( schemaOptions . toObject && transform ) ) {
const opts = options . json ? schemaOptions . toJSON : schemaOptions . toObject ;
if ( opts ) {
transform = ( typeof options . transform === 'function' ? options . transform : opts . transform ) ;
}
} else {
options . transform = originalTransform ;
}
if ( typeof transform === 'function' ) {
const xformed = transform ( this , ret , options ) ;
if ( typeof xformed !== 'undefined' ) {
ret = xformed ;
}
}
return ret ;
} ;
/ * *
* Converts this document into a plain javascript object , ready for storage in MongoDB .
*
* Buffers are converted to instances of [ mongodb . Binary ] ( http : //mongodb.github.com/node-mongodb-native/api-bson-generated/binary.html) for proper storage.
*
* # # # # Options :
*
* - ` getters ` apply all getters ( path and virtual getters ) , defaults to false
* - ` virtuals ` apply virtual getters ( can override ` getters ` option ) , defaults to false
* - ` minimize ` remove empty objects ( defaults to true )
* - ` transform ` a transform function to apply to the resulting document before returning
* - ` depopulate ` depopulate any populated paths , replacing them with their original refs ( defaults to false )
* - ` versionKey ` whether to include the version key ( defaults to true )
*
* # # # # Getters / Virtuals
*
* Example of only applying path getters
*
* doc . toObject ( { getters : true , virtuals : false } )
*
* Example of only applying virtual getters
*
* doc . toObject ( { virtuals : true } )
*
* Example of applying both path and virtual getters
*
* doc . toObject ( { getters : true } )
*
* To apply these options to every document of your schema by default , set your [ schemas ] ( # schema _Schema ) ` toObject ` option to the same argument .
*
* schema . set ( 'toObject' , { virtuals : true } )
*
* # # # # Transform
*
* We may need to perform a transformation of the resulting object based on some criteria , say to remove some sensitive information or return a custom object . In this case we set the optional ` transform ` function .
*
* Transform functions receive three arguments
*
* function ( doc , ret , options ) { }
*
* - ` doc ` The mongoose document which is being converted
* - ` ret ` The plain object representation which has been converted
* - ` options ` The options in use ( either schema options or the options passed inline )
*
* # # # # Example
*
* // specify the transform schema option
* if ( ! schema . options . toObject ) schema . options . toObject = { } ;
* schema . options . toObject . transform = function ( doc , ret , options ) {
* // remove the _id of every document before returning the result
* delete ret . _id ;
* return ret ;
* }
*
* // without the transformation in the schema
* doc . toObject ( ) ; // { _id: 'anId', name: 'Wreck-it Ralph' }
*
* // with the transformation
* doc . toObject ( ) ; // { name: 'Wreck-it Ralph' }
*
* With transformations we can do a lot more than remove properties . We can even return completely new customized objects :
*
* if ( ! schema . options . toObject ) schema . options . toObject = { } ;
* schema . options . toObject . transform = function ( doc , ret , options ) {
* return { movie : ret . name }
* }
*
* // without the transformation in the schema
* doc . toObject ( ) ; // { _id: 'anId', name: 'Wreck-it Ralph' }
*
* // with the transformation
* doc . toObject ( ) ; // { movie: 'Wreck-it Ralph' }
*
* _Note : if a transform function returns ` undefined ` , the return value will be ignored . _
*
* Transformations may also be applied inline , overridding any transform set in the options :
*
* function xform ( doc , ret , options ) {
* return { inline : ret . name , custom : true }
* }
*
* // pass the transform as an inline option
* doc . toObject ( { transform : xform } ) ; // { inline: 'Wreck-it Ralph', custom: true }
*
* If you want to skip transformations , use ` transform: false ` :
*
* if ( ! schema . options . toObject ) schema . options . toObject = { } ;
* schema . options . toObject . hide = '_id' ;
* schema . options . toObject . transform = function ( doc , ret , options ) {
* if ( options . hide ) {
* options . hide . split ( ' ' ) . forEach ( function ( prop ) {
* delete ret [ prop ] ;
* } ) ;
* }
* return ret ;
* }
*
* var doc = new Doc ( { _id : 'anId' , secret : 47 , name : 'Wreck-it Ralph' } ) ;
* doc . toObject ( ) ; // { secret: 47, name: 'Wreck-it Ralph' }
* doc . toObject ( { hide : 'secret _id' , transform : false } ) ; // { _id: 'anId', secret: 47, name: 'Wreck-it Ralph' }
* doc . toObject ( { hide : 'secret _id' , transform : true } ) ; // { name: 'Wreck-it Ralph' }
*
* Transforms are applied _only to the document and are not applied to sub - documents _ .
*
2019-07-02 16:05:15 +02:00
* Transforms , like all of these options , are also available for ` toJSON ` . See [ this guide to ` JSON.stringify() ` ] ( https : //thecodebarbarian.com/the-80-20-guide-to-json-stringify-in-javascript.html) to learn why `toJSON()` and `toObject()` are separate functions.
2019-02-01 14:06:44 +01:00
*
* See [ schema options ] ( / d o c s / g u i d e . h t m l # t o O b j e c t ) f o r s o m e m o r e d e t a i l s .
*
* _During save , no custom options are applied to the document before being sent to the database . _
*
* @ param { Object } [ options ]
* @ param { Boolean } [ options . getters = false ] if true , apply all getters , including virtuals
* @ param { Boolean } [ options . virtuals = false ] if true , apply virtuals . Use ` { getters: true, virtuals: false } ` to just apply getters , not virtuals
* @ param { Boolean } [ options . minimize = true ] if true , omit any empty objects from the output
* @ param { Function | null } [ options . transform = null ] if set , mongoose will call this function to allow you to transform the returned object
* @ param { Boolean } [ options . depopulate = false ] if true , replace any conventionally populated paths with the original id in the output . Has no affect on virtual populated paths .
* @ param { Boolean } [ options . versionKey = true ] if false , exclude the version key ( ` __v ` by default ) from the output
* @ param { Boolean } [ options . flattenMaps = false ] if true , convert Maps to POJOs . Useful if you want to ` JSON.stringify() ` the result of ` toObject() ` .
* @ return { Object } js object
* @ see mongodb . Binary http : //mongodb.github.com/node-mongodb-native/api-bson-generated/binary.html
* @ api public
* @ memberOf Document
* @ instance
* /
Document . prototype . toObject = function ( options ) {
return this . $toObject ( options ) ;
} ;
/ * !
* Minimizes an object , removing undefined values and empty objects
*
* @ param { Object } object to minimize
* @ return { Object }
* /
function minimize ( obj ) {
const keys = Object . keys ( obj ) ;
let i = keys . length ;
let hasKeys ;
let key ;
let val ;
while ( i -- ) {
key = keys [ i ] ;
val = obj [ key ] ;
if ( utils . isObject ( val ) && ! Buffer . isBuffer ( val ) ) {
obj [ key ] = minimize ( val ) ;
}
if ( undefined === obj [ key ] ) {
delete obj [ key ] ;
continue ;
}
hasKeys = true ;
}
return hasKeys
? obj
: undefined ;
}
/ * !
* Applies virtuals properties to ` json ` .
*
* @ param { Document } self
* @ param { Object } json
* @ param { String } type either ` virtuals ` or ` paths `
* @ return { Object } ` json `
* /
function applyGetters ( self , json , type , options ) {
const schema = self . schema ;
const paths = Object . keys ( schema [ type ] ) ;
let i = paths . length ;
const numPaths = i ;
let path ;
let assignPath ;
let cur = self . _doc ;
let v ;
if ( ! cur ) {
return json ;
}
if ( type === 'virtuals' ) {
options = options || { } ;
for ( i = 0 ; i < numPaths ; ++ i ) {
path = paths [ i ] ;
// We may be applying virtuals to a nested object, for example if calling
// `doc.nestedProp.toJSON()`. If so, the path we assign to, `assignPath`,
// will be a trailing substring of the `path`.
assignPath = path ;
if ( options . path != null ) {
if ( ! path . startsWith ( options . path + '.' ) ) {
continue ;
}
assignPath = path . substr ( options . path . length + 1 ) ;
}
const parts = assignPath . split ( '.' ) ;
v = clone ( self . get ( path ) , options ) ;
if ( v === void 0 ) {
continue ;
}
const plen = parts . length ;
cur = json ;
for ( let j = 0 ; j < plen - 1 ; ++ j ) {
cur [ parts [ j ] ] = cur [ parts [ j ] ] || { } ;
cur = cur [ parts [ j ] ] ;
}
cur [ parts [ plen - 1 ] ] = v ;
}
return json ;
}
while ( i -- ) {
path = paths [ i ] ;
const parts = path . split ( '.' ) ;
const plen = parts . length ;
const last = plen - 1 ;
let branch = json ;
let part ;
cur = self . _doc ;
for ( let ii = 0 ; ii < plen ; ++ ii ) {
part = parts [ ii ] ;
v = cur [ part ] ;
if ( ii === last ) {
2019-04-17 15:58:15 +02:00
const val = self . get ( path ) ;
// Ignore single nested docs: getters will run because of `clone()`
// before `applyGetters()` in `$toObject()`. Quirk because single
// nested subdocs are hydrated docs in `_doc` as opposed to POJOs.
if ( val != null && val . $ _ _ == null ) {
branch [ part ] = clone ( val , options ) ;
}
2019-02-01 14:06:44 +01:00
} else if ( v == null ) {
if ( part in cur ) {
branch [ part ] = v ;
}
break ;
} else {
branch = branch [ part ] || ( branch [ part ] = { } ) ;
}
cur = v ;
}
}
return json ;
}
/ * *
* The return value of this method is used in calls to JSON . stringify ( doc ) .
*
* This method accepts the same options as [ Document # toObject ] ( # document _Document - toObject ) . To apply the options to every document of your schema by default , set your [ schemas ] ( # schema _Schema ) ` toJSON ` option to the same argument .
*
* schema . set ( 'toJSON' , { virtuals : true } )
*
* See [ schema options ] ( / d o c s / g u i d e . h t m l # t o J S O N ) f o r d e t a i l s .
*
* @ param { Object } options
* @ return { Object }
* @ see Document # toObject # document _Document - toObject
2019-07-02 16:05:15 +02:00
* @ see JSON . stringify ( ) in JavaScript https : //thecodebarbarian.com/the-80-20-guide-to-json-stringify-in-javascript.html
2019-02-01 14:06:44 +01:00
* @ api public
* @ memberOf Document
* @ instance
* /
Document . prototype . toJSON = function ( options ) {
return this . $toObject ( options , true ) ;
} ;
/ * *
* Helper for console . log
*
* @ api public
* @ method inspect
* @ memberOf Document
* @ instance
* /
Document . prototype . inspect = function ( options ) {
2019-06-04 14:29:48 +02:00
const isPOJO = utils . isPOJO ( options ) ;
2019-02-01 14:06:44 +01:00
let opts ;
if ( isPOJO ) {
opts = options ;
opts . minimize = false ;
}
return this . toObject ( opts ) ;
} ;
if ( inspect . custom ) {
/ * !
* Avoid Node deprecation warning DEP0079
* /
Document . prototype [ inspect . custom ] = Document . prototype . inspect ;
}
/ * *
* Helper for console . log
*
* @ api public
* @ method toString
* @ memberOf Document
* @ instance
* /
Document . prototype . toString = function ( ) {
return inspect ( this . inspect ( ) ) ;
} ;
/ * *
* Returns true if the Document stores the same data as doc .
*
* Documents are considered equal when they have matching ` _id ` s , unless neither
* document has an ` _id ` , in which case this function falls back to using
* ` deepEqual() ` .
*
* @ param { Document } doc a document to compare
* @ return { Boolean }
* @ api public
* @ memberOf Document
* @ instance
* /
Document . prototype . equals = function ( doc ) {
if ( ! doc ) {
return false ;
}
const tid = this . get ( '_id' ) ;
const docid = doc . get ? doc . get ( '_id' ) : doc ;
if ( ! tid && ! docid ) {
return deepEqual ( this , doc ) ;
}
return tid && tid . equals
? tid . equals ( docid )
: tid === docid ;
} ;
/ * *
* Populates document references , executing the ` callback ` when complete .
* If you want to use promises instead , use this function with
* [ ` execPopulate() ` ] ( # document _Document - execPopulate )
*
* # # # # Example :
*
* doc
* . populate ( 'company' )
* . populate ( {
* path : 'notes' ,
* match : /airline/ ,
* select : 'text' ,
* model : 'modelName'
* options : opts
* } , function ( err , user ) {
* assert ( doc . _id === user . _id ) // the document itself is passed
* } )
*
* // summary
* doc . populate ( path ) // not executed
* doc . populate ( options ) ; // not executed
* doc . populate ( path , callback ) // executed
* doc . populate ( options , callback ) ; // executed
* doc . populate ( callback ) ; // executed
* doc . populate ( options ) . execPopulate ( ) // executed, returns promise
*
*
* # # # # NOTE :
*
* Population does not occur unless a ` callback ` is passed * or * you explicitly
* call ` execPopulate() ` .
* Passing the same path a second time will overwrite the previous path options .
* See [ Model . populate ( ) ] ( # model _Model . populate ) for explaination of options .
*
* @ see Model . populate # model _Model . populate
* @ see Document . execPopulate # document _Document - execPopulate
* @ param { String | Object } [ path ] The path to populate or an options object
* @ param { Function } [ callback ] When passed , population is invoked
* @ api public
* @ return { Document } this
* @ memberOf Document
* @ instance
* /
Document . prototype . populate = function populate ( ) {
if ( arguments . length === 0 ) {
return this ;
}
const pop = this . $ _ _ . populate || ( this . $ _ _ . populate = { } ) ;
const args = utils . args ( arguments ) ;
let fn ;
if ( typeof args [ args . length - 1 ] === 'function' ) {
fn = args . pop ( ) ;
}
// allow `doc.populate(callback)`
if ( args . length ) {
// use hash to remove duplicate paths
const res = utils . populate . apply ( null , args ) ;
for ( let i = 0 ; i < res . length ; ++ i ) {
pop [ res [ i ] . path ] = res [ i ] ;
}
}
if ( fn ) {
const paths = utils . object . vals ( pop ) ;
this . $ _ _ . populate = undefined ;
let topLevelModel = this . constructor ;
if ( this . $ _ _isNested ) {
topLevelModel = this . $ _ _ . scope . constructor ;
const nestedPath = this . $ _ _ . nestedPath ;
paths . forEach ( function ( populateOptions ) {
populateOptions . path = nestedPath + '.' + populateOptions . path ;
} ) ;
}
// Use `$session()` by default if the document has an associated session
// See gh-6754
if ( this . $session ( ) != null ) {
const session = this . $session ( ) ;
paths . forEach ( path => {
if ( path . options == null ) {
path . options = { session : session } ;
return ;
}
if ( ! ( 'session' in path . options ) ) {
path . options . session = session ;
}
} ) ;
}
topLevelModel . populate ( this , paths , fn ) ;
}
return this ;
} ;
/ * *
* Explicitly executes population and returns a promise . Useful for ES2015
* integration .
*
* # # # # Example :
*
* var promise = doc .
* populate ( 'company' ) .
* populate ( {
* path : 'notes' ,
* match : /airline/ ,
* select : 'text' ,
* model : 'modelName'
* options : opts
* } ) .
* execPopulate ( ) ;
*
* // summary
* doc . execPopulate ( ) . then ( resolve , reject ) ;
*
*
* @ see Document . populate # document _Document - populate
* @ api public
* @ param { Function } [ callback ] optional callback . If specified , a promise will * * not * * be returned
* @ return { Promise } promise that resolves to the document when population is done
* @ memberOf Document
* @ instance
* /
Document . prototype . execPopulate = function ( callback ) {
return utils . promiseOrCallback ( callback , cb => {
this . populate ( cb ) ;
} , this . constructor . events ) ;
} ;
/ * *
* Gets _id ( s ) used during population of the given ` path ` .
*
* # # # # Example :
*
* Model . findOne ( ) . populate ( 'author' ) . exec ( function ( err , doc ) {
* console . log ( doc . author . name ) // Dr.Seuss
* console . log ( doc . populated ( 'author' ) ) // '5144cf8050f071d979c118a7'
* } )
*
* If the path was not populated , undefined is returned .
*
* @ param { String } path
* @ return { Array | ObjectId | Number | Buffer | String | undefined }
* @ memberOf Document
* @ instance
* @ api public
* /
Document . prototype . populated = function ( path , val , options ) {
// val and options are internal
if ( val === null || val === void 0 ) {
if ( ! this . $ _ _ . populated ) {
return undefined ;
}
const v = this . $ _ _ . populated [ path ] ;
if ( v ) {
return v . value ;
}
return undefined ;
}
// internal
if ( val === true ) {
if ( ! this . $ _ _ . populated ) {
return undefined ;
}
return this . $ _ _ . populated [ path ] ;
}
this . $ _ _ . populated || ( this . $ _ _ . populated = { } ) ;
this . $ _ _ . populated [ path ] = { value : val , options : options } ;
2019-06-04 14:29:48 +02:00
// If this was a nested populate, make sure each populated doc knows
// about its populated children (gh-7685)
const pieces = path . split ( '.' ) ;
for ( let i = 0 ; i < pieces . length - 1 ; ++ i ) {
const subpath = pieces . slice ( 0 , i + 1 ) . join ( '.' ) ;
const subdoc = this . get ( subpath ) ;
if ( subdoc != null && subdoc . $ _ _ != null && this . populated ( subpath ) ) {
const rest = pieces . slice ( i + 1 ) . join ( '.' ) ;
subdoc . populated ( rest , val , options ) ;
// No need to continue because the above recursion should take care of
// marking the rest of the docs as populated
break ;
}
}
2019-02-01 14:06:44 +01:00
return val ;
} ;
/ * *
* Takes a populated field and returns it to its unpopulated state .
*
* # # # # Example :
*
* Model . findOne ( ) . populate ( 'author' ) . exec ( function ( err , doc ) {
* console . log ( doc . author . name ) ; // Dr.Seuss
* console . log ( doc . depopulate ( 'author' ) ) ;
* console . log ( doc . author ) ; // '5144cf8050f071d979c118a7'
* } )
*
* If the path was not populated , this is a no - op .
*
* @ param { String } path
* @ return { Document } this
* @ see Document . populate # document _Document - populate
* @ api public
* @ memberOf Document
* @ instance
* /
Document . prototype . depopulate = function ( path ) {
if ( typeof path === 'string' ) {
path = path . split ( ' ' ) ;
}
let populatedIds ;
const virtualKeys = this . $$populatedVirtuals ? Object . keys ( this . $$populatedVirtuals ) : [ ] ;
2019-04-17 15:58:15 +02:00
const populated = get ( this , '$__.populated' , { } ) ;
2019-02-01 14:06:44 +01:00
if ( arguments . length === 0 ) {
// Depopulate all
2019-04-17 15:58:15 +02:00
for ( let i = 0 ; i < virtualKeys . length ; i ++ ) {
2019-02-01 14:06:44 +01:00
delete this . $$populatedVirtuals [ virtualKeys [ i ] ] ;
delete this . _doc [ virtualKeys [ i ] ] ;
2019-04-17 15:58:15 +02:00
delete populated [ virtualKeys [ i ] ] ;
2019-02-01 14:06:44 +01:00
}
2019-04-17 15:58:15 +02:00
const keys = Object . keys ( populated ) ;
for ( let i = 0 ; i < keys . length ; i ++ ) {
2019-02-01 14:06:44 +01:00
populatedIds = this . populated ( keys [ i ] ) ;
if ( ! populatedIds ) {
continue ;
}
2019-04-17 15:58:15 +02:00
delete populated [ keys [ i ] ] ;
2019-02-01 14:06:44 +01:00
this . $set ( keys [ i ] , populatedIds ) ;
}
return this ;
}
2019-04-17 15:58:15 +02:00
for ( let i = 0 ; i < path . length ; i ++ ) {
2019-02-01 14:06:44 +01:00
populatedIds = this . populated ( path [ i ] ) ;
2019-04-17 15:58:15 +02:00
delete populated [ path [ i ] ] ;
if ( virtualKeys . indexOf ( path [ i ] ) !== - 1 ) {
delete this . $$populatedVirtuals [ path [ i ] ] ;
delete this . _doc [ path [ i ] ] ;
2019-06-04 14:29:48 +02:00
} else if ( populatedIds ) {
2019-04-17 15:58:15 +02:00
this . $set ( path [ i ] , populatedIds ) ;
2019-02-01 14:06:44 +01:00
}
}
return this ;
} ;
/ * *
* Returns the full path to this document .
*
* @ param { String } [ path ]
* @ return { String }
* @ api private
* @ method $ _ _fullPath
* @ memberOf Document
* @ instance
* /
Document . prototype . $ _ _fullPath = function ( path ) {
// overridden in SubDocuments
return path || '' ;
} ;
/ * !
* Module exports .
* /
Document . ValidationError = ValidationError ;
module . exports = exports = Document ;