mongoose
Version:
Mongoose MongoDB ODM
369 lines (318 loc) • 9.79 kB
JavaScript
'use strict';
const Mixed = require('../schema/mixed');
const MongooseError = require('../error/mongooseError');
const clone = require('../helpers/clone');
const deepEqual = require('../utils').deepEqual;
const getConstructorName = require('../helpers/getConstructorName');
const handleSpreadDoc = require('../helpers/document/handleSpreadDoc');
const util = require('util');
const specialProperties = require('../helpers/specialProperties');
const isBsonType = require('../helpers/isBsonType');
const cleanModifiedSubpaths = require('../helpers/document/cleanModifiedSubpaths');
const populateModelSymbol = require('../helpers/symbols').populateModelSymbol;
/*!
* ignore
*/
class MongooseMap extends Map {
constructor(v, path, doc, schemaType) {
if (getConstructorName(v) === 'Object') {
v = Object.keys(v).reduce((arr, key) => arr.concat([[key, v[key]]]), []);
}
super(v);
this.$__parent = doc != null && doc.$__ != null ? doc : null;
this.$__path = path;
this.$__schemaType = schemaType == null ? new Mixed(path) : schemaType;
this.$__runDeferred();
}
$init(key, value) {
checkValidKey(key);
super.set(key, value);
if (value != null && value.$isSingleNested) {
value.$basePath = this.$__path + '.' + key;
}
}
$__set(key, value) {
super.set(key, value);
}
/**
* Overwrites native Map's `get()` function to support Mongoose getters.
*
* @api public
* @method get
* @memberOf Map
*/
get(key, options) {
if (isBsonType(key, 'ObjectId')) {
key = key.toString();
}
options = options || {};
if (options.getters === false) {
return super.get(key);
}
return this.$__schemaType.applyGetters(super.get(key), this.$__parent);
}
/**
* Overwrites native Map's `set()` function to support setters, `populate()`,
* and change tracking. Note that Mongoose maps _only_ support strings and
* ObjectIds as keys.
*
* Keys also cannot:
* - be named after special properties `prototype`, `constructor`, and `__proto__`
* - start with a dollar sign (`$`)
* - contain any dots (`.`)
*
* #### Example:
*
* doc.myMap.set('test', 42); // works
* doc.myMap.set({ obj: 42 }, 42); // Throws "Mongoose maps only support string keys"
* doc.myMap.set(10, 42); // Throws "Mongoose maps only support string keys"
* doc.myMap.set("$test", 42); // Throws "Mongoose maps do not support keys that start with "$", got "$test""
*
* @api public
* @method set
* @memberOf Map
*/
set(key, value) {
if (isBsonType(key, 'ObjectId')) {
key = key.toString();
}
checkValidKey(key);
value = handleSpreadDoc(value);
// Weird, but because you can't assign to `this` before calling `super()`
// you can't get access to `$__schemaType` to cast in the initial call to
// `set()` from the `super()` constructor.
if (this.$__schemaType == null) {
this.$__deferred = this.$__deferred || [];
this.$__deferred.push({ key: key, value: value });
return;
}
let _fullPath;
const parent = this.$__parent;
const populated = parent != null && parent.$__ && parent.$__.populated ?
parent.$populated(fullPath.call(this), true) || parent.$populated(this.$__path, true) :
null;
const priorVal = this.get(key);
if (populated != null) {
if (this.$__schemaType.$isSingleNested) {
throw new MongooseError(
'Cannot manually populate single nested subdoc underneath Map ' +
`at path "${this.$__path}". Try using an array instead of a Map.`
);
}
if (Array.isArray(value) && this.$__schemaType.$isMongooseArray) {
value = value.map(v => {
if (v.$__ == null) {
v = new populated.options[populateModelSymbol](v);
}
// Doesn't support single nested "in-place" populate
v.$__.wasPopulated = { value: v._doc._id };
return v;
});
} else if (value != null) {
if (value.$__ == null) {
value = new populated.options[populateModelSymbol](value);
}
// Doesn't support single nested "in-place" populate
value.$__.wasPopulated = { value: value._doc._id };
}
} else {
try {
const options = this.$__schemaType.$isMongooseDocumentArray || this.$__schemaType.$isSingleNested ?
{ path: fullPath.call(this) } :
null;
value = this.$__schemaType.applySetters(
value,
this.$__parent,
false,
this.get(key),
options
);
} catch (error) {
if (this.$__parent != null && this.$__parent.$__ != null) {
this.$__parent.invalidate(fullPath.call(this), error);
return;
}
throw error;
}
}
super.set(key, value);
if (parent != null && parent.$__ != null && !deepEqual(value, priorVal)) {
const path = fullPath.call(this);
parent.markModified(path);
// If overwriting the full document array or subdoc, make sure to clean up any paths that were modified
// before re: #15108
if (this.$__schemaType.$isMongooseDocumentArray || this.$__schemaType.$isSingleNested) {
cleanModifiedSubpaths(parent, path);
}
}
// Delay calculating full path unless absolutely necessary, because string
// concatenation is a bottleneck re: #13171
function fullPath() {
if (_fullPath) {
return _fullPath;
}
_fullPath = this.$__path + '.' + key;
return _fullPath;
}
}
/**
* Overwrites native Map's `clear()` function to support change tracking.
*
* @api public
* @method clear
* @memberOf Map
*/
clear() {
super.clear();
const parent = this.$__parent;
if (parent != null) {
parent.markModified(this.$__path);
}
}
/**
* Overwrites native Map's `delete()` function to support change tracking.
*
* @api public
* @method delete
* @memberOf Map
*/
delete(key) {
if (isBsonType(key, 'ObjectId')) {
key = key.toString();
}
this.set(key, undefined);
return super.delete(key);
}
/**
* Converts this map to a native JavaScript Map so the MongoDB driver can serialize it.
*
* @api public
* @method toBSON
* @memberOf Map
*/
toBSON() {
return new Map(this);
}
toObject(options) {
if (options && options.flattenMaps) {
const ret = {};
const keys = this.keys();
for (const key of keys) {
ret[key] = clone(this.get(key), options);
}
return ret;
}
return new Map(this);
}
$toObject() {
return this.constructor.prototype.toObject.apply(this, arguments);
}
/**
* Converts this map to a native JavaScript Map for `JSON.stringify()`. Set
* the `flattenMaps` option to convert this map to a POJO instead.
*
* #### Example:
*
* doc.myMap.toJSON() instanceof Map; // true
* doc.myMap.toJSON({ flattenMaps: true }) instanceof Map; // false
*
* @api public
* @method toJSON
* @param {Object} [options]
* @param {Boolean} [options.flattenMaps=false] set to `true` to convert the map to a POJO rather than a native JavaScript map
* @memberOf Map
*/
toJSON(options) {
if (typeof (options && options.flattenMaps) === 'boolean' ? options.flattenMaps : true) {
const ret = {};
const keys = this.keys();
for (const key of keys) {
ret[key] = clone(this.get(key), options);
}
return ret;
}
return new Map(this);
}
inspect() {
return new Map(this);
}
$__runDeferred() {
if (!this.$__deferred) {
return;
}
for (const keyValueObject of this.$__deferred) {
this.set(keyValueObject.key, keyValueObject.value);
}
this.$__deferred = null;
}
}
if (util.inspect.custom) {
Object.defineProperty(MongooseMap.prototype, util.inspect.custom, {
enumerable: false,
writable: false,
configurable: false,
value: MongooseMap.prototype.inspect
});
}
Object.defineProperty(MongooseMap.prototype, '$__set', {
enumerable: false,
writable: true,
configurable: false
});
Object.defineProperty(MongooseMap.prototype, '$__parent', {
enumerable: false,
writable: true,
configurable: false
});
Object.defineProperty(MongooseMap.prototype, '$__path', {
enumerable: false,
writable: true,
configurable: false
});
Object.defineProperty(MongooseMap.prototype, '$__schemaType', {
enumerable: false,
writable: true,
configurable: false
});
/**
* Set to `true` for all Mongoose map instances
*
* @api public
* @property $isMongooseMap
* @memberOf MongooseMap
* @instance
*/
Object.defineProperty(MongooseMap.prototype, '$isMongooseMap', {
enumerable: false,
writable: false,
configurable: false,
value: true
});
Object.defineProperty(MongooseMap.prototype, '$__deferredCalls', {
enumerable: false,
writable: false,
configurable: false,
value: true
});
/**
* Since maps are stored as objects under the hood, keys must be strings
* and can't contain any invalid characters
* @param {String} key
* @api private
*/
function checkValidKey(key) {
const keyType = typeof key;
if (keyType !== 'string') {
throw new TypeError(`Mongoose maps only support string keys, got ${keyType}`);
}
if (key.startsWith('$')) {
throw new Error(`Mongoose maps do not support keys that start with "$", got "${key}"`);
}
if (key.includes('.')) {
throw new Error(`Mongoose maps do not support keys that contain ".", got "${key}"`);
}
if (specialProperties.has(key)) {
throw new Error(`Mongoose maps do not support reserved key name "${key}"`);
}
}
module.exports = MongooseMap;