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('./helpers/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
86 // gh-6027: if we haven't found the schematype but this path is
87 // underneath an embedded discriminator and the embedded discriminator
88 // key is in the query, use the embedded discriminator schema
89 if (_schematype != null &&
90 get(_schematype, 'schema.discriminators') != null &&
91 discriminatorKey != null &&
92 pathLastHalf !== discriminatorKey) {
93 const discriminatorVal = get(obj, pathFirstHalf + '.' + discriminatorKey);
94 if (discriminatorVal != null) {
95 schematype = _schematype.schema.discriminators[discriminatorVal].
96 path(pathLastHalf);
97 }
98 }
99 }
100 }
101
102 if (!schematype) {
103 // Handle potential embedded array queries
104 const split = path.split('.');
105 let j = split.length;
106 let pathFirstHalf;
107 let pathLastHalf;
108 let remainingConds;
109
110 // Find the part of the var path that is a path of the Schema
111 while (j--) {
112 pathFirstHalf = split.slice(0, j).join('.');
113 schematype = schema.path(pathFirstHalf);
114 if (schematype) {
115 break;
116 }
117 }
118
119 // If a substring of the input path resolves to an actual real path...
120 if (schematype) {
121 // Apply the casting; similar code for $elemMatch in schema/array.js
122 if (schematype.caster && schematype.caster.schema) {
123 remainingConds = {};
124 pathLastHalf = split.slice(j).join('.');
125 remainingConds[pathLastHalf] = val;
126 obj[path] = cast(schematype.caster.schema, remainingConds, options, context)[pathLastHalf];
127 } else {
128 obj[path] = val;
129 }
130 continue;
131 }
132
133 if (utils.isObject(val)) {
134 // handle geo schemas that use object notation
135 // { loc: { long: Number, lat: Number }
136
137 let geo = '';
138 if (val.$near) {
139 geo = '$near';
140 } else if (val.$nearSphere) {
141 geo = '$nearSphere';
142 } else if (val.$within) {
143 geo = '$within';
144 } else if (val.$geoIntersects) {
145 geo = '$geoIntersects';
146 } else if (val.$geoWithin) {
147 geo = '$geoWithin';
148 }
149
150 if (geo) {
151 const numbertype = new Types.Number('__QueryCasting__');
152 let value = val[geo];
153
154 if (val.$maxDistance != null) {
155 val.$maxDistance = numbertype.castForQueryWrapper({
156 val: val.$maxDistance,
157 context: context
158 });
159 }
160 if (val.$minDistance != null) {
161 val.$minDistance = numbertype.castForQueryWrapper({
162 val: val.$minDistance,
163 context: context
164 });
165 }
166
167 if (geo === '$within') {
168 const withinType = value.$center
169 || value.$centerSphere
170 || value.$box
171 || value.$polygon;
172
173 if (!withinType) {
174 throw new Error('Bad $within parameter: ' + JSON.stringify(val));
175 }
176
177 value = withinType;
178 } else if (geo === '$near' &&
179 typeof value.type === 'string' && Array.isArray(value.coordinates)) {
180 // geojson; cast the coordinates
181 value = value.coordinates;
182 } else if ((geo === '$near' || geo === '$nearSphere' || geo === '$geoIntersects') &&
183 value.$geometry && typeof value.$geometry.type === 'string' &&
184 Array.isArray(value.$geometry.coordinates)) {
185 if (value.$maxDistance != null) {
186 value.$maxDistance = numbertype.castForQueryWrapper({
187 val: value.$maxDistance,
188 context: context
189 });
190 }
191 if (value.$minDistance != null) {
192 value.$minDistance = numbertype.castForQueryWrapper({
193 val: value.$minDistance,
194 context: context
195 });
196 }
197 if (utils.isMongooseObject(value.$geometry)) {
198 value.$geometry = value.$geometry.toObject({
199 transform: false,
200 virtuals: false
201 });
202 }
203 value = value.$geometry.coordinates;
204 } else if (geo === '$geoWithin') {
205 if (value.$geometry) {
206 if (utils.isMongooseObject(value.$geometry)) {
207 value.$geometry = value.$geometry.toObject({ virtuals: false });
208 }
209 const geoWithinType = value.$geometry.type;
210 if (ALLOWED_GEOWITHIN_GEOJSON_TYPES.indexOf(geoWithinType) === -1) {
211 throw new Error('Invalid geoJSON type for $geoWithin "' +
212 geoWithinType + '", must be "Polygon" or "MultiPolygon"');
213 }
214 value = value.$geometry.coordinates;
215 } else {
216 value = value.$box || value.$polygon || value.$center ||
217 value.$centerSphere;
218 if (utils.isMongooseObject(value)) {
219 value = value.toObject({ virtuals: false });
220 }
221 }
222 }
223
224 _cast(value, numbertype, context);
225 continue;
226 }
227 }
228
229 if (schema.nested[path]) {
230 continue;
231 }
232 if (options.upsert && options.strict) {
233 if (options.strict === 'throw') {
234 throw new StrictModeError(path);
235 }
236 throw new StrictModeError(path, 'Path "' + path + '" is not in ' +
237 'schema, strict mode is `true`, and upsert is `true`.');
238 } else if (options.strictQuery === 'throw') {
239 throw new StrictModeError(path, 'Path "' + path + '" is not in ' +
240 'schema and strictQuery is \'throw\'.');
241 } else if (options.strictQuery) {
242 delete obj[path];
243 }
244 } else if (val == null) {
245 continue;
246 } else if (val.constructor.name === 'Object') {
247 any$conditionals = Object.keys(val).some(function(k) {
248 return k.startsWith('$') && k !== '$id' && k !== '$ref';
249 });
250
251 if (!any$conditionals) {
252 obj[path] = schematype.castForQueryWrapper({
253 val: val,
254 context: context
255 });
256 } else {
257 const ks = Object.keys(val);
258 let $cond;
259
260 let k = ks.length;
261
262 while (k--) {
263 $cond = ks[k];
264 nested = val[$cond];
265
266 if ($cond === '$not') {
267 if (nested && schematype && !schematype.caster) {
268 _keys = Object.keys(nested);
269 if (_keys.length && _keys[0].startsWith('$')) {
270 for (const key in nested) {
271 nested[key] = schematype.castForQueryWrapper({
272 $conditional: key,
273 val: nested[key],
274 context: context
275 });
276 }
277 } else {
278 val[$cond] = schematype.castForQueryWrapper({
279 $conditional: $cond,
280 val: nested,
281 context: context
282 });
283 }
284 continue;
285 }
286 cast(schematype.caster ? schematype.caster.schema : schema, nested, options, context);
287 } else {
288 val[$cond] = schematype.castForQueryWrapper({
289 $conditional: $cond,
290 val: nested,
291 context: context
292 });
293 }
294 }
295 }
296 } else if (Array.isArray(val) && ['Buffer', 'Array'].indexOf(schematype.instance) === -1) {
297 const casted = [];
298 for (let valIndex = 0; valIndex < val.length; valIndex++) {
299 casted.push(schematype.castForQueryWrapper({
300 val: val[valIndex],
301 context: context
302 }));
303 }
304
305 obj[path] = { $in: casted };
306 } else {
307 obj[path] = schematype.castForQueryWrapper({
308 val: val,
309 context: context
310 });
311 }
312 }
313 }
314
315 return obj;
316};
317
318function _cast(val, numbertype, context) {
319 if (Array.isArray(val)) {
320 val.forEach(function(item, i) {
321 if (Array.isArray(item) || utils.isObject(item)) {
322 return _cast(item, numbertype, context);
323 }
324 val[i] = numbertype.castForQueryWrapper({ val: item, context: context });
325 });
326 } else {
327 const nearKeys = Object.keys(val);
328 let nearLen = nearKeys.length;
329 while (nearLen--) {
330 const nkey = nearKeys[nearLen];
331 const item = val[nkey];
332 if (Array.isArray(item) || utils.isObject(item)) {
333 _cast(item, numbertype, context);
334 val[nkey] = item;
335 } else {
336 val[nkey] = numbertype.castForQuery({ val: item, context: context });
337 }
338 }
339 }
340}
\No newline at end of file