UNPKG

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