embedded.js 8.2 KB

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