UNPKG

14.5 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 cast$expr = require('./helpers/query/cast$expr');
11const castTextSearch = require('./schema/operators/text');
12const get = require('./helpers/get');
13const getSchemaDiscriminatorByValue = require('./helpers/discriminator/getSchemaDiscriminatorByValue');
14const isOperator = require('./helpers/query/isOperator');
15const util = require('util');
16const isObject = require('./helpers/isObject');
17const isMongooseObject = require('./helpers/isMongooseObject');
18const utils = require('./utils');
19
20const ALLOWED_GEOWITHIN_GEOJSON_TYPES = ['Polygon', 'MultiPolygon'];
21
22/**
23 * Handles internal casting for query filters.
24 *
25 * @param {Schema} schema
26 * @param {Object} obj Object to cast
27 * @param {Object} [options] the query options
28 * @param {Boolean|"throw"} [options.strict] Wheter to enable all strict options
29 * @param {Boolean|"throw"} [options.strictQuery] Enable strict Queries
30 * @param {Boolean} [options.upsert]
31 * @param {Query} [context] passed to setters
32 * @api private
33 */
34module.exports = function cast(schema, obj, options, context) {
35 if (Array.isArray(obj)) {
36 throw new Error('Query filter must be an object, got an array ', util.inspect(obj));
37 }
38
39 if (obj == null) {
40 return obj;
41 }
42
43 if (schema != null && schema.discriminators != null && obj[schema.options.discriminatorKey] != null) {
44 schema = getSchemaDiscriminatorByValue(schema, obj[schema.options.discriminatorKey]) || schema;
45 }
46
47 const paths = Object.keys(obj);
48 let i = paths.length;
49 let _keys;
50 let any$conditionals;
51 let schematype;
52 let nested;
53 let path;
54 let type;
55 let val;
56
57 options = options || {};
58
59 while (i--) {
60 path = paths[i];
61 val = obj[path];
62
63 if (path === '$or' || path === '$nor' || path === '$and') {
64 if (!Array.isArray(val)) {
65 throw new CastError('Array', val, path);
66 }
67 for (let k = 0; k < val.length; ++k) {
68 if (val[k] == null || typeof val[k] !== 'object') {
69 throw new CastError('Object', val[k], path + '.' + k);
70 }
71 const discriminatorValue = val[k][schema.options.discriminatorKey];
72 if (discriminatorValue == null) {
73 val[k] = cast(schema, val[k], options, context);
74 } else {
75 const discriminatorSchema = getSchemaDiscriminatorByValue(context.schema, discriminatorValue);
76 val[k] = cast(discriminatorSchema ? discriminatorSchema : schema, val[k], options, context);
77 }
78 }
79 } else if (path === '$where') {
80 type = typeof val;
81
82 if (type !== 'string' && type !== 'function') {
83 throw new Error('Must have a string or function for $where');
84 }
85
86 if (type === 'function') {
87 obj[path] = val.toString();
88 }
89
90 continue;
91 } else if (path === '$expr') {
92 val = cast$expr(val, schema);
93 continue;
94 } else if (path === '$elemMatch') {
95 val = cast(schema, val, options, context);
96 } else if (path === '$text') {
97 val = castTextSearch(val, path);
98 } else {
99 if (!schema) {
100 // no casting for Mixed types
101 continue;
102 }
103
104 schematype = schema.path(path);
105
106 // Check for embedded discriminator paths
107 if (!schematype) {
108 const split = path.split('.');
109 let j = split.length;
110 while (j--) {
111 const pathFirstHalf = split.slice(0, j).join('.');
112 const pathLastHalf = split.slice(j).join('.');
113 const _schematype = schema.path(pathFirstHalf);
114 const discriminatorKey = _schematype &&
115 _schematype.schema &&
116 _schematype.schema.options &&
117 _schematype.schema.options.discriminatorKey;
118
119 // gh-6027: if we haven't found the schematype but this path is
120 // underneath an embedded discriminator and the embedded discriminator
121 // key is in the query, use the embedded discriminator schema
122 if (_schematype != null &&
123 (_schematype.schema && _schematype.schema.discriminators) != null &&
124 discriminatorKey != null &&
125 pathLastHalf !== discriminatorKey) {
126 const discriminatorVal = get(obj, pathFirstHalf + '.' + discriminatorKey);
127 const discriminators = _schematype.schema.discriminators;
128 if (typeof discriminatorVal === 'string' && discriminators[discriminatorVal] != null) {
129
130 schematype = discriminators[discriminatorVal].path(pathLastHalf);
131 } else if (discriminatorVal != null &&
132 Object.keys(discriminatorVal).length === 1 &&
133 Array.isArray(discriminatorVal.$in) &&
134 discriminatorVal.$in.length === 1 &&
135 typeof discriminatorVal.$in[0] === 'string' &&
136 discriminators[discriminatorVal.$in[0]] != null) {
137 schematype = discriminators[discriminatorVal.$in[0]].path(pathLastHalf);
138 }
139 }
140 }
141 }
142
143 if (!schematype) {
144 // Handle potential embedded array queries
145 const split = path.split('.');
146 let j = split.length;
147 let pathFirstHalf;
148 let pathLastHalf;
149 let remainingConds;
150
151 // Find the part of the var path that is a path of the Schema
152 while (j--) {
153 pathFirstHalf = split.slice(0, j).join('.');
154 schematype = schema.path(pathFirstHalf);
155 if (schematype) {
156 break;
157 }
158 }
159
160 // If a substring of the input path resolves to an actual real path...
161 if (schematype) {
162 // Apply the casting; similar code for $elemMatch in schema/array.js
163 if (schematype.caster && schematype.caster.schema) {
164 remainingConds = {};
165 pathLastHalf = split.slice(j).join('.');
166 remainingConds[pathLastHalf] = val;
167
168 const ret = cast(schematype.caster.schema, remainingConds, options, context)[pathLastHalf];
169 if (ret === void 0) {
170 delete obj[path];
171 } else {
172 obj[path] = ret;
173 }
174 } else {
175 obj[path] = val;
176 }
177 continue;
178 }
179
180 if (isObject(val)) {
181 // handle geo schemas that use object notation
182 // { loc: { long: Number, lat: Number }
183
184 let geo = '';
185 if (val.$near) {
186 geo = '$near';
187 } else if (val.$nearSphere) {
188 geo = '$nearSphere';
189 } else if (val.$within) {
190 geo = '$within';
191 } else if (val.$geoIntersects) {
192 geo = '$geoIntersects';
193 } else if (val.$geoWithin) {
194 geo = '$geoWithin';
195 }
196
197 if (geo) {
198 const numbertype = new Types.Number('__QueryCasting__');
199 let value = val[geo];
200
201 if (val.$maxDistance != null) {
202 val.$maxDistance = numbertype.castForQuery(
203 null,
204 val.$maxDistance,
205 context
206 );
207 }
208 if (val.$minDistance != null) {
209 val.$minDistance = numbertype.castForQuery(
210 null,
211 val.$minDistance,
212 context
213 );
214 }
215
216 if (geo === '$within') {
217 const withinType = value.$center
218 || value.$centerSphere
219 || value.$box
220 || value.$polygon;
221
222 if (!withinType) {
223 throw new Error('Bad $within parameter: ' + JSON.stringify(val));
224 }
225
226 value = withinType;
227 } else if (geo === '$near' &&
228 typeof value.type === 'string' && Array.isArray(value.coordinates)) {
229 // geojson; cast the coordinates
230 value = value.coordinates;
231 } else if ((geo === '$near' || geo === '$nearSphere' || geo === '$geoIntersects') &&
232 value.$geometry && typeof value.$geometry.type === 'string' &&
233 Array.isArray(value.$geometry.coordinates)) {
234 if (value.$maxDistance != null) {
235 value.$maxDistance = numbertype.castForQuery(
236 null,
237 value.$maxDistance,
238 context
239 );
240 }
241 if (value.$minDistance != null) {
242 value.$minDistance = numbertype.castForQuery(
243 null,
244 value.$minDistance,
245 context
246 );
247 }
248 if (isMongooseObject(value.$geometry)) {
249 value.$geometry = value.$geometry.toObject({
250 transform: false,
251 virtuals: false
252 });
253 }
254 value = value.$geometry.coordinates;
255 } else if (geo === '$geoWithin') {
256 if (value.$geometry) {
257 if (isMongooseObject(value.$geometry)) {
258 value.$geometry = value.$geometry.toObject({ virtuals: false });
259 }
260 const geoWithinType = value.$geometry.type;
261 if (ALLOWED_GEOWITHIN_GEOJSON_TYPES.indexOf(geoWithinType) === -1) {
262 throw new Error('Invalid geoJSON type for $geoWithin "' +
263 geoWithinType + '", must be "Polygon" or "MultiPolygon"');
264 }
265 value = value.$geometry.coordinates;
266 } else {
267 value = value.$box || value.$polygon || value.$center ||
268 value.$centerSphere;
269 if (isMongooseObject(value)) {
270 value = value.toObject({ virtuals: false });
271 }
272 }
273 }
274
275 _cast(value, numbertype, context);
276 continue;
277 }
278 }
279
280 if (schema.nested[path]) {
281 continue;
282 }
283
284 const strict = 'strict' in options ? options.strict : schema.options.strict;
285 const strictQuery = getStrictQuery(options, schema._userProvidedOptions, schema.options, context);
286 if (options.upsert && strict) {
287 if (strict === 'throw') {
288 throw new StrictModeError(path);
289 }
290 throw new StrictModeError(path, 'Path "' + path + '" is not in ' +
291 'schema, strict mode is `true`, and upsert is `true`.');
292 } if (strictQuery === 'throw') {
293 throw new StrictModeError(path, 'Path "' + path + '" is not in ' +
294 'schema and strictQuery is \'throw\'.');
295 } else if (strictQuery) {
296 delete obj[path];
297 }
298 } else if (val == null) {
299 continue;
300 } else if (utils.isPOJO(val)) {
301 any$conditionals = Object.keys(val).some(isOperator);
302
303 if (!any$conditionals) {
304 obj[path] = schematype.castForQuery(
305 null,
306 val,
307 context
308 );
309 } else {
310 const ks = Object.keys(val);
311 let $cond;
312 let k = ks.length;
313
314 while (k--) {
315 $cond = ks[k];
316 nested = val[$cond];
317 if ($cond === '$elemMatch') {
318 if (nested && schematype != null && schematype.schema != null) {
319 cast(schematype.schema, nested, options, context);
320 } else if (nested && schematype != null && schematype.$isMongooseArray) {
321 if (utils.isPOJO(nested) && nested.$not != null) {
322 cast(schema, nested, options, context);
323 } else {
324 val[$cond] = schematype.castForQuery(
325 $cond,
326 nested,
327 context
328 );
329 }
330 }
331 } else if ($cond === '$not') {
332 if (nested && schematype) {
333 _keys = Object.keys(nested);
334 if (_keys.length && isOperator(_keys[0])) {
335 for (const key in nested) {
336 nested[key] = schematype.castForQuery(
337 key,
338 nested[key],
339 context
340 );
341 }
342 } else {
343 val[$cond] = schematype.castForQuery(
344 $cond,
345 nested,
346 context
347 );
348 }
349 continue;
350 }
351 } else {
352 val[$cond] = schematype.castForQuery(
353 $cond,
354 nested,
355 context
356 );
357 }
358
359 }
360 }
361 } else if (Array.isArray(val) && ['Buffer', 'Array'].indexOf(schematype.instance) === -1) {
362 const casted = [];
363 const valuesArray = val;
364
365 for (const _val of valuesArray) {
366 casted.push(schematype.castForQuery(
367 null,
368 _val,
369 context
370 ));
371 }
372
373 obj[path] = { $in: casted };
374 } else {
375 obj[path] = schematype.castForQuery(
376 null,
377 val,
378 context
379 );
380 }
381 }
382 }
383
384 return obj;
385};
386
387function _cast(val, numbertype, context) {
388 if (Array.isArray(val)) {
389 val.forEach(function(item, i) {
390 if (Array.isArray(item) || isObject(item)) {
391 return _cast(item, numbertype, context);
392 }
393 val[i] = numbertype.castForQuery(null, item, context);
394 });
395 } else {
396 const nearKeys = Object.keys(val);
397 let nearLen = nearKeys.length;
398 while (nearLen--) {
399 const nkey = nearKeys[nearLen];
400 const item = val[nkey];
401 if (Array.isArray(item) || isObject(item)) {
402 _cast(item, numbertype, context);
403 val[nkey] = item;
404 } else {
405 val[nkey] = numbertype.castForQuery({ val: item, context: context });
406 }
407 }
408 }
409}
410
411function getStrictQuery(queryOptions, schemaUserProvidedOptions, schemaOptions, context) {
412 if ('strictQuery' in queryOptions) {
413 return queryOptions.strictQuery;
414 }
415 if ('strictQuery' in schemaUserProvidedOptions) {
416 return schemaUserProvidedOptions.strictQuery;
417 }
418 const mongooseOptions = context &&
419 context.mongooseCollection &&
420 context.mongooseCollection.conn &&
421 context.mongooseCollection.conn.base &&
422 context.mongooseCollection.conn.base.options;
423 if (mongooseOptions) {
424 if ('strictQuery' in mongooseOptions) {
425 return mongooseOptions.strictQuery;
426 }
427 }
428 return schemaOptions.strictQuery;
429}