embedded.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. 'use strict';
  2. /*!
  3. * Module dependencies.
  4. */
  5. const CastError = require('../error/cast');
  6. const EventEmitter = require('events').EventEmitter;
  7. const ObjectExpectedError = require('../error/objectExpected');
  8. const SchemaType = require('../schematype');
  9. const $exists = require('./operators/exists');
  10. const castToNumber = require('./operators/helpers').castToNumber;
  11. const discriminator = require('../helpers/model/discriminator');
  12. const geospatial = require('./operators/geospatial');
  13. const get = require('../helpers/get');
  14. const getDiscriminatorByValue = require('../queryhelpers').getDiscriminatorByValue;
  15. const internalToObjectOptions = require('../options').internalToObjectOptions;
  16. let Subdocument;
  17. module.exports = Embedded;
  18. /**
  19. * Sub-schema schematype constructor
  20. *
  21. * @param {Schema} schema
  22. * @param {String} key
  23. * @param {Object} options
  24. * @inherits SchemaType
  25. * @api public
  26. */
  27. function Embedded(schema, path, options) {
  28. this.caster = _createConstructor(schema);
  29. this.caster.path = path;
  30. this.caster.prototype.$basePath = path;
  31. this.schema = schema;
  32. this.$isSingleNested = true;
  33. SchemaType.call(this, path, options, 'Embedded');
  34. }
  35. /*!
  36. * ignore
  37. */
  38. Embedded.prototype = Object.create(SchemaType.prototype);
  39. Embedded.prototype.constructor = Embedded;
  40. /*!
  41. * ignore
  42. */
  43. function _createConstructor(schema) {
  44. // lazy load
  45. Subdocument || (Subdocument = require('../types/subdocument'));
  46. const _embedded = function SingleNested(value, path, parent) {
  47. const _this = this;
  48. this.$parent = parent;
  49. Subdocument.apply(this, arguments);
  50. this.$session(this.ownerDocument().$session());
  51. if (parent) {
  52. parent.on('save', function() {
  53. _this.emit('save', _this);
  54. _this.constructor.emit('save', _this);
  55. });
  56. parent.on('isNew', function(val) {
  57. _this.isNew = val;
  58. _this.emit('isNew', val);
  59. _this.constructor.emit('isNew', val);
  60. });
  61. }
  62. };
  63. _embedded.prototype = Object.create(Subdocument.prototype);
  64. _embedded.prototype.$__setSchema(schema);
  65. _embedded.prototype.constructor = _embedded;
  66. _embedded.schema = schema;
  67. _embedded.$isSingleNested = true;
  68. _embedded.events = new EventEmitter();
  69. _embedded.prototype.toBSON = function() {
  70. return this.toObject(internalToObjectOptions);
  71. };
  72. // apply methods
  73. for (const i in schema.methods) {
  74. _embedded.prototype[i] = schema.methods[i];
  75. }
  76. // apply statics
  77. for (const i in schema.statics) {
  78. _embedded[i] = schema.statics[i];
  79. }
  80. for (const i in EventEmitter.prototype) {
  81. _embedded[i] = EventEmitter.prototype[i];
  82. }
  83. return _embedded;
  84. }
  85. /*!
  86. * Special case for when users use a common location schema to represent
  87. * locations for use with $geoWithin.
  88. * https://docs.mongodb.org/manual/reference/operator/query/geoWithin/
  89. *
  90. * @param {Object} val
  91. * @api private
  92. */
  93. Embedded.prototype.$conditionalHandlers.$geoWithin = function handle$geoWithin(val) {
  94. return { $geometry: this.castForQuery(val.$geometry) };
  95. };
  96. /*!
  97. * ignore
  98. */
  99. Embedded.prototype.$conditionalHandlers.$near =
  100. Embedded.prototype.$conditionalHandlers.$nearSphere = geospatial.cast$near;
  101. Embedded.prototype.$conditionalHandlers.$within =
  102. Embedded.prototype.$conditionalHandlers.$geoWithin = geospatial.cast$within;
  103. Embedded.prototype.$conditionalHandlers.$geoIntersects =
  104. geospatial.cast$geoIntersects;
  105. Embedded.prototype.$conditionalHandlers.$minDistance = castToNumber;
  106. Embedded.prototype.$conditionalHandlers.$maxDistance = castToNumber;
  107. Embedded.prototype.$conditionalHandlers.$exists = $exists;
  108. /**
  109. * Casts contents
  110. *
  111. * @param {Object} value
  112. * @api private
  113. */
  114. Embedded.prototype.cast = function(val, doc, init, priorVal) {
  115. if (val && val.$isSingleNested) {
  116. return val;
  117. }
  118. if (val != null && (typeof val !== 'object' || Array.isArray(val))) {
  119. throw new ObjectExpectedError(this.path, val);
  120. }
  121. let Constructor = this.caster;
  122. const discriminatorKey = Constructor.schema.options.discriminatorKey;
  123. if (val != null &&
  124. Constructor.discriminators &&
  125. typeof val[discriminatorKey] === 'string') {
  126. if (Constructor.discriminators[val[discriminatorKey]]) {
  127. Constructor = Constructor.discriminators[val[discriminatorKey]];
  128. } else {
  129. const constructorByValue = getDiscriminatorByValue(Constructor, val[discriminatorKey]);
  130. if (constructorByValue) {
  131. Constructor = constructorByValue;
  132. }
  133. }
  134. }
  135. let subdoc;
  136. // Only pull relevant selected paths and pull out the base path
  137. const parentSelected = get(doc, '$__.selected', {});
  138. const path = this.path;
  139. const selected = Object.keys(parentSelected).reduce((obj, key) => {
  140. if (key.startsWith(path + '.')) {
  141. obj[key.substr(path.length + 1)] = parentSelected[key];
  142. }
  143. return obj;
  144. }, {});
  145. if (init) {
  146. subdoc = new Constructor(void 0, selected, doc);
  147. subdoc.init(val);
  148. } else {
  149. if (Object.keys(val).length === 0) {
  150. return new Constructor({}, selected, doc);
  151. }
  152. return new Constructor(val, selected, doc, undefined, { priorDoc: priorVal });
  153. }
  154. return subdoc;
  155. };
  156. /**
  157. * Casts contents for query
  158. *
  159. * @param {string} [$conditional] optional query operator (like `$eq` or `$in`)
  160. * @param {any} value
  161. * @api private
  162. */
  163. Embedded.prototype.castForQuery = function($conditional, val) {
  164. let handler;
  165. if (arguments.length === 2) {
  166. handler = this.$conditionalHandlers[$conditional];
  167. if (!handler) {
  168. throw new Error('Can\'t use ' + $conditional);
  169. }
  170. return handler.call(this, val);
  171. }
  172. val = $conditional;
  173. if (val == null) {
  174. return val;
  175. }
  176. if (this.options.runSetters) {
  177. val = this._applySetters(val);
  178. }
  179. let Constructor = this.caster;
  180. const discriminatorKey = Constructor.schema.options.discriminatorKey;
  181. if (val != null &&
  182. Constructor.discriminators &&
  183. typeof val[discriminatorKey] === 'string') {
  184. if (Constructor.discriminators[val[discriminatorKey]]) {
  185. Constructor = Constructor.discriminators[val[discriminatorKey]];
  186. } else {
  187. const constructorByValue = getDiscriminatorByValue(Constructor, val[discriminatorKey]);
  188. if (constructorByValue) {
  189. Constructor = constructorByValue;
  190. }
  191. }
  192. }
  193. try {
  194. val = new Constructor(val);
  195. } catch (error) {
  196. // Make sure we always wrap in a CastError (gh-6803)
  197. if (!(error instanceof CastError)) {
  198. throw new CastError('Embedded', val, this.path, error);
  199. }
  200. throw error;
  201. }
  202. return val;
  203. };
  204. /**
  205. * Async validation on this single nested doc.
  206. *
  207. * @api private
  208. */
  209. Embedded.prototype.doValidate = function(value, fn, scope, options) {
  210. let Constructor = this.caster;
  211. const discriminatorKey = Constructor.schema.options.discriminatorKey;
  212. if (value != null &&
  213. Constructor.discriminators &&
  214. typeof value[discriminatorKey] === 'string') {
  215. if (Constructor.discriminators[value[discriminatorKey]]) {
  216. Constructor = Constructor.discriminators[value[discriminatorKey]];
  217. } else {
  218. const constructorByValue = getDiscriminatorByValue(Constructor, value[discriminatorKey]);
  219. if (constructorByValue) {
  220. Constructor = constructorByValue;
  221. }
  222. }
  223. }
  224. if (options && options.skipSchemaValidators) {
  225. if (!(value instanceof Constructor)) {
  226. value = new Constructor(value, null, scope);
  227. }
  228. return value.validate(fn);
  229. }
  230. SchemaType.prototype.doValidate.call(this, value, function(error) {
  231. if (error) {
  232. return fn(error);
  233. }
  234. if (!value) {
  235. return fn(null);
  236. }
  237. value.validate(fn);
  238. }, scope);
  239. };
  240. /**
  241. * Synchronously validate this single nested doc
  242. *
  243. * @api private
  244. */
  245. Embedded.prototype.doValidateSync = function(value, scope, options) {
  246. if (!options || !options.skipSchemaValidators) {
  247. const schemaTypeError = SchemaType.prototype.doValidateSync.call(this, value, scope);
  248. if (schemaTypeError) {
  249. return schemaTypeError;
  250. }
  251. }
  252. if (!value) {
  253. return;
  254. }
  255. return value.validateSync();
  256. };
  257. /**
  258. * Adds a discriminator to this property
  259. *
  260. * @param {String} name
  261. * @param {Schema} schema fields to add to the schema for instances of this sub-class
  262. * @api public
  263. */
  264. Embedded.prototype.discriminator = function(name, schema) {
  265. discriminator(this.caster, name, schema);
  266. this.caster.discriminators[name] = _createConstructor(schema);
  267. return this.caster.discriminators[name];
  268. };
  269. /*!
  270. * ignore
  271. */
  272. Embedded.prototype.clone = function() {
  273. const options = Object.assign({}, this.options, { validators: this.validators });
  274. return new this.constructor(this.schema, this.path, options);
  275. };