cast.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. 'use strict';
  2. /*!
  3. * Module dependencies.
  4. */
  5. const StrictModeError = require('./error/strict');
  6. const Types = require('./schema/index');
  7. const castTextSearch = require('./schema/operators/text');
  8. const get = require('./helpers/get');
  9. const util = require('util');
  10. const utils = require('./utils');
  11. const ALLOWED_GEOWITHIN_GEOJSON_TYPES = ['Polygon', 'MultiPolygon'];
  12. /**
  13. * Handles internal casting for query filters.
  14. *
  15. * @param {Schema} schema
  16. * @param {Object} obj Object to cast
  17. * @param {Object} options the query options
  18. * @param {Query} context passed to setters
  19. * @api private
  20. */
  21. module.exports = function cast(schema, obj, options, context) {
  22. if (Array.isArray(obj)) {
  23. throw new Error('Query filter must be an object, got an array ', util.inspect(obj));
  24. }
  25. const paths = Object.keys(obj);
  26. let i = paths.length;
  27. let _keys;
  28. let any$conditionals;
  29. let schematype;
  30. let nested;
  31. let path;
  32. let type;
  33. let val;
  34. options = options || {};
  35. while (i--) {
  36. path = paths[i];
  37. val = obj[path];
  38. if (path === '$or' || path === '$nor' || path === '$and') {
  39. let k = val.length;
  40. while (k--) {
  41. val[k] = cast(schema, val[k], options, context);
  42. }
  43. } else if (path === '$where') {
  44. type = typeof val;
  45. if (type !== 'string' && type !== 'function') {
  46. throw new Error('Must have a string or function for $where');
  47. }
  48. if (type === 'function') {
  49. obj[path] = val.toString();
  50. }
  51. continue;
  52. } else if (path === '$elemMatch') {
  53. val = cast(schema, val, options, context);
  54. } else if (path === '$text') {
  55. val = castTextSearch(val, path);
  56. } else {
  57. if (!schema) {
  58. // no casting for Mixed types
  59. continue;
  60. }
  61. schematype = schema.path(path);
  62. // Check for embedded discriminator paths
  63. if (!schematype) {
  64. const split = path.split('.');
  65. let j = split.length;
  66. while (j--) {
  67. const pathFirstHalf = split.slice(0, j).join('.');
  68. const pathLastHalf = split.slice(j).join('.');
  69. const _schematype = schema.path(pathFirstHalf);
  70. const discriminatorKey = get(_schematype, 'schema.options.discriminatorKey');
  71. // gh-6027: if we haven't found the schematype but this path is
  72. // underneath an embedded discriminator and the embedded discriminator
  73. // key is in the query, use the embedded discriminator schema
  74. if (_schematype != null &&
  75. get(_schematype, 'schema.discriminators') != null &&
  76. discriminatorKey != null &&
  77. pathLastHalf !== discriminatorKey) {
  78. const discriminatorVal = get(obj, pathFirstHalf + '.' + discriminatorKey);
  79. if (discriminatorVal != null) {
  80. schematype = _schematype.schema.discriminators[discriminatorVal].
  81. path(pathLastHalf);
  82. }
  83. }
  84. }
  85. }
  86. if (!schematype) {
  87. // Handle potential embedded array queries
  88. const split = path.split('.');
  89. let j = split.length;
  90. let pathFirstHalf;
  91. let pathLastHalf;
  92. let remainingConds;
  93. // Find the part of the var path that is a path of the Schema
  94. while (j--) {
  95. pathFirstHalf = split.slice(0, j).join('.');
  96. schematype = schema.path(pathFirstHalf);
  97. if (schematype) {
  98. break;
  99. }
  100. }
  101. // If a substring of the input path resolves to an actual real path...
  102. if (schematype) {
  103. // Apply the casting; similar code for $elemMatch in schema/array.js
  104. if (schematype.caster && schematype.caster.schema) {
  105. remainingConds = {};
  106. pathLastHalf = split.slice(j).join('.');
  107. remainingConds[pathLastHalf] = val;
  108. obj[path] = cast(schematype.caster.schema, remainingConds, options, context)[pathLastHalf];
  109. } else {
  110. obj[path] = val;
  111. }
  112. continue;
  113. }
  114. if (utils.isObject(val)) {
  115. // handle geo schemas that use object notation
  116. // { loc: { long: Number, lat: Number }
  117. let geo = '';
  118. if (val.$near) {
  119. geo = '$near';
  120. } else if (val.$nearSphere) {
  121. geo = '$nearSphere';
  122. } else if (val.$within) {
  123. geo = '$within';
  124. } else if (val.$geoIntersects) {
  125. geo = '$geoIntersects';
  126. } else if (val.$geoWithin) {
  127. geo = '$geoWithin';
  128. }
  129. if (geo) {
  130. const numbertype = new Types.Number('__QueryCasting__');
  131. let value = val[geo];
  132. if (val.$maxDistance != null) {
  133. val.$maxDistance = numbertype.castForQueryWrapper({
  134. val: val.$maxDistance,
  135. context: context
  136. });
  137. }
  138. if (val.$minDistance != null) {
  139. val.$minDistance = numbertype.castForQueryWrapper({
  140. val: val.$minDistance,
  141. context: context
  142. });
  143. }
  144. if (geo === '$within') {
  145. const withinType = value.$center
  146. || value.$centerSphere
  147. || value.$box
  148. || value.$polygon;
  149. if (!withinType) {
  150. throw new Error('Bad $within parameter: ' + JSON.stringify(val));
  151. }
  152. value = withinType;
  153. } else if (geo === '$near' &&
  154. typeof value.type === 'string' && Array.isArray(value.coordinates)) {
  155. // geojson; cast the coordinates
  156. value = value.coordinates;
  157. } else if ((geo === '$near' || geo === '$nearSphere' || geo === '$geoIntersects') &&
  158. value.$geometry && typeof value.$geometry.type === 'string' &&
  159. Array.isArray(value.$geometry.coordinates)) {
  160. if (value.$maxDistance != null) {
  161. value.$maxDistance = numbertype.castForQueryWrapper({
  162. val: value.$maxDistance,
  163. context: context
  164. });
  165. }
  166. if (value.$minDistance != null) {
  167. value.$minDistance = numbertype.castForQueryWrapper({
  168. val: value.$minDistance,
  169. context: context
  170. });
  171. }
  172. if (utils.isMongooseObject(value.$geometry)) {
  173. value.$geometry = value.$geometry.toObject({
  174. transform: false,
  175. virtuals: false
  176. });
  177. }
  178. value = value.$geometry.coordinates;
  179. } else if (geo === '$geoWithin') {
  180. if (value.$geometry) {
  181. if (utils.isMongooseObject(value.$geometry)) {
  182. value.$geometry = value.$geometry.toObject({ virtuals: false });
  183. }
  184. const geoWithinType = value.$geometry.type;
  185. if (ALLOWED_GEOWITHIN_GEOJSON_TYPES.indexOf(geoWithinType) === -1) {
  186. throw new Error('Invalid geoJSON type for $geoWithin "' +
  187. geoWithinType + '", must be "Polygon" or "MultiPolygon"');
  188. }
  189. value = value.$geometry.coordinates;
  190. } else {
  191. value = value.$box || value.$polygon || value.$center ||
  192. value.$centerSphere;
  193. if (utils.isMongooseObject(value)) {
  194. value = value.toObject({ virtuals: false });
  195. }
  196. }
  197. }
  198. _cast(value, numbertype, context);
  199. continue;
  200. }
  201. }
  202. if (schema.nested[path]) {
  203. continue;
  204. }
  205. if (options.upsert && options.strict) {
  206. if (options.strict === 'throw') {
  207. throw new StrictModeError(path);
  208. }
  209. throw new StrictModeError(path, 'Path "' + path + '" is not in ' +
  210. 'schema, strict mode is `true`, and upsert is `true`.');
  211. } else if (options.strictQuery === 'throw') {
  212. throw new StrictModeError(path, 'Path "' + path + '" is not in ' +
  213. 'schema and strictQuery is true.');
  214. } else if (options.strictQuery) {
  215. delete obj[path];
  216. }
  217. } else if (val == null) {
  218. continue;
  219. } else if (val.constructor.name === 'Object') {
  220. any$conditionals = Object.keys(val).some(function(k) {
  221. return k.charAt(0) === '$' && k !== '$id' && k !== '$ref';
  222. });
  223. if (!any$conditionals) {
  224. obj[path] = schematype.castForQueryWrapper({
  225. val: val,
  226. context: context
  227. });
  228. } else {
  229. const ks = Object.keys(val);
  230. let $cond;
  231. let k = ks.length;
  232. while (k--) {
  233. $cond = ks[k];
  234. nested = val[$cond];
  235. if ($cond === '$not') {
  236. if (nested && schematype && !schematype.caster) {
  237. _keys = Object.keys(nested);
  238. if (_keys.length && _keys[0].charAt(0) === '$') {
  239. for (const key in nested) {
  240. nested[key] = schematype.castForQueryWrapper({
  241. $conditional: key,
  242. val: nested[key],
  243. context: context
  244. });
  245. }
  246. } else {
  247. val[$cond] = schematype.castForQueryWrapper({
  248. $conditional: $cond,
  249. val: nested,
  250. context: context
  251. });
  252. }
  253. continue;
  254. }
  255. cast(schematype.caster ? schematype.caster.schema : schema, nested, options, context);
  256. } else {
  257. val[$cond] = schematype.castForQueryWrapper({
  258. $conditional: $cond,
  259. val: nested,
  260. context: context
  261. });
  262. }
  263. }
  264. }
  265. } else if (Array.isArray(val) && ['Buffer', 'Array'].indexOf(schematype.instance) === -1) {
  266. const casted = [];
  267. for (let valIndex = 0; valIndex < val.length; valIndex++) {
  268. casted.push(schematype.castForQueryWrapper({
  269. val: val[valIndex],
  270. context: context
  271. }));
  272. }
  273. obj[path] = { $in: casted };
  274. } else {
  275. obj[path] = schematype.castForQueryWrapper({
  276. val: val,
  277. context: context
  278. });
  279. }
  280. }
  281. }
  282. return obj;
  283. };
  284. function _cast(val, numbertype, context) {
  285. if (Array.isArray(val)) {
  286. val.forEach(function(item, i) {
  287. if (Array.isArray(item) || utils.isObject(item)) {
  288. return _cast(item, numbertype, context);
  289. }
  290. val[i] = numbertype.castForQueryWrapper({ val: item, context: context });
  291. });
  292. } else {
  293. const nearKeys = Object.keys(val);
  294. let nearLen = nearKeys.length;
  295. while (nearLen--) {
  296. const nkey = nearKeys[nearLen];
  297. const item = val[nkey];
  298. if (Array.isArray(item) || utils.isObject(item)) {
  299. _cast(item, numbertype, context);
  300. val[nkey] = item;
  301. } else {
  302. val[nkey] = numbertype.castForQuery({ val: item, context: context });
  303. }
  304. }
  305. }
  306. }