UNPKG

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