UNPKG

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