map.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. 'use strict';
  2. const Mixed = require('../schema/mixed');
  3. const clone = require('../helpers/clone');
  4. const deepEqual = require('../utils').deepEqual;
  5. const getConstructorName = require('../helpers/getConstructorName');
  6. const handleSpreadDoc = require('../helpers/document/handleSpreadDoc');
  7. const util = require('util');
  8. const specialProperties = require('../helpers/specialProperties');
  9. const isBsonType = require('../helpers/isBsonType');
  10. const populateModelSymbol = require('../helpers/symbols').populateModelSymbol;
  11. /*!
  12. * ignore
  13. */
  14. class MongooseMap extends Map {
  15. constructor(v, path, doc, schemaType) {
  16. if (getConstructorName(v) === 'Object') {
  17. v = Object.keys(v).reduce((arr, key) => arr.concat([[key, v[key]]]), []);
  18. }
  19. super(v);
  20. this.$__parent = doc != null && doc.$__ != null ? doc : null;
  21. this.$__path = path;
  22. this.$__schemaType = schemaType == null ? new Mixed(path) : schemaType;
  23. this.$__runDeferred();
  24. }
  25. $init(key, value) {
  26. checkValidKey(key);
  27. super.set(key, value);
  28. if (value != null && value.$isSingleNested) {
  29. value.$basePath = this.$__path + '.' + key;
  30. }
  31. }
  32. $__set(key, value) {
  33. super.set(key, value);
  34. }
  35. get(key, options) {
  36. if (isBsonType(key, 'ObjectID')) {
  37. key = key.toString();
  38. }
  39. options = options || {};
  40. if (options.getters === false) {
  41. return super.get(key);
  42. }
  43. return this.$__schemaType.applyGetters(super.get(key), this.$__parent);
  44. }
  45. set(key, value) {
  46. if (isBsonType(key, 'ObjectID')) {
  47. key = key.toString();
  48. }
  49. checkValidKey(key);
  50. value = handleSpreadDoc(value);
  51. // Weird, but because you can't assign to `this` before calling `super()`
  52. // you can't get access to `$__schemaType` to cast in the initial call to
  53. // `set()` from the `super()` constructor.
  54. if (this.$__schemaType == null) {
  55. this.$__deferred = this.$__deferred || [];
  56. this.$__deferred.push({ key: key, value: value });
  57. return;
  58. }
  59. const fullPath = this.$__path + '.' + key;
  60. const populated = this.$__parent != null && this.$__parent.$__ ?
  61. this.$__parent.$populated(fullPath) || this.$__parent.$populated(this.$__path) :
  62. null;
  63. const priorVal = this.get(key);
  64. if (populated != null) {
  65. if (value.$__ == null) {
  66. value = new populated.options[populateModelSymbol](value);
  67. }
  68. value.$__.wasPopulated = { value: populated.value };
  69. } else {
  70. try {
  71. value = this.$__schemaType.
  72. applySetters(value, this.$__parent, false, this.get(key), { path: fullPath });
  73. } catch (error) {
  74. if (this.$__parent != null && this.$__parent.$__ != null) {
  75. this.$__parent.invalidate(fullPath, error);
  76. return;
  77. }
  78. throw error;
  79. }
  80. }
  81. super.set(key, value);
  82. if (value != null && value.$isSingleNested) {
  83. value.$basePath = this.$__path + '.' + key;
  84. }
  85. const parent = this.$__parent;
  86. if (parent != null && parent.$__ != null && !deepEqual(value, priorVal)) {
  87. parent.markModified(this.$__path + '.' + key);
  88. }
  89. }
  90. clear() {
  91. super.clear();
  92. const parent = this.$__parent;
  93. if (parent != null) {
  94. parent.markModified(this.$__path);
  95. }
  96. }
  97. delete(key) {
  98. if (isBsonType(key, 'ObjectID')) {
  99. key = key.toString();
  100. }
  101. this.set(key, undefined);
  102. super.delete(key);
  103. }
  104. toBSON() {
  105. return new Map(this);
  106. }
  107. toObject(options) {
  108. if (options && options.flattenMaps) {
  109. const ret = {};
  110. const keys = this.keys();
  111. for (const key of keys) {
  112. ret[key] = clone(this.get(key), options);
  113. }
  114. return ret;
  115. }
  116. return new Map(this);
  117. }
  118. $toObject() {
  119. return this.constructor.prototype.toObject.apply(this, arguments);
  120. }
  121. toJSON(options) {
  122. if (typeof (options && options.flattenMaps) === 'boolean' ? options.flattenMaps : true) {
  123. const ret = {};
  124. const keys = this.keys();
  125. for (const key of keys) {
  126. ret[key] = clone(this.get(key), options);
  127. }
  128. return ret;
  129. }
  130. return new Map(this);
  131. }
  132. inspect() {
  133. return new Map(this);
  134. }
  135. $__runDeferred() {
  136. if (!this.$__deferred) {
  137. return;
  138. }
  139. for (const keyValueObject of this.$__deferred) {
  140. this.set(keyValueObject.key, keyValueObject.value);
  141. }
  142. this.$__deferred = null;
  143. }
  144. }
  145. if (util.inspect.custom) {
  146. Object.defineProperty(MongooseMap.prototype, util.inspect.custom, {
  147. enumerable: false,
  148. writable: false,
  149. configurable: false,
  150. value: MongooseMap.prototype.inspect
  151. });
  152. }
  153. Object.defineProperty(MongooseMap.prototype, '$__set', {
  154. enumerable: false,
  155. writable: true,
  156. configurable: false
  157. });
  158. Object.defineProperty(MongooseMap.prototype, '$__parent', {
  159. enumerable: false,
  160. writable: true,
  161. configurable: false
  162. });
  163. Object.defineProperty(MongooseMap.prototype, '$__path', {
  164. enumerable: false,
  165. writable: true,
  166. configurable: false
  167. });
  168. Object.defineProperty(MongooseMap.prototype, '$__schemaType', {
  169. enumerable: false,
  170. writable: true,
  171. configurable: false
  172. });
  173. Object.defineProperty(MongooseMap.prototype, '$isMongooseMap', {
  174. enumerable: false,
  175. writable: false,
  176. configurable: false,
  177. value: true
  178. });
  179. Object.defineProperty(MongooseMap.prototype, '$__deferredCalls', {
  180. enumerable: false,
  181. writable: false,
  182. configurable: false,
  183. value: true
  184. });
  185. /*!
  186. * Since maps are stored as objects under the hood, keys must be strings
  187. * and can't contain any invalid characters
  188. */
  189. function checkValidKey(key) {
  190. const keyType = typeof key;
  191. if (keyType !== 'string') {
  192. throw new TypeError(`Mongoose maps only support string keys, got ${keyType}`);
  193. }
  194. if (key.startsWith('$')) {
  195. throw new Error(`Mongoose maps do not support keys that start with "$", got "${key}"`);
  196. }
  197. if (key.includes('.')) {
  198. throw new Error(`Mongoose maps do not support keys that contain ".", got "${key}"`);
  199. }
  200. if (specialProperties.has(key)) {
  201. throw new Error(`Mongoose maps do not support reserved key name "${key}"`);
  202. }
  203. }
  204. module.exports = MongooseMap;