1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 | module.exports = ModelUtils;
|
11 |
|
12 |
|
13 |
|
14 |
|
15 | const g = require('strong-globalize')();
|
16 | const geo = require('./geo');
|
17 | const {
|
18 | fieldsToArray,
|
19 | sanitizeQuery: sanitizeQueryOrData,
|
20 | isPlainObject,
|
21 | isClass,
|
22 | toRegExp,
|
23 | } = require('./utils');
|
24 | const BaseModel = require('./model');
|
25 |
|
26 |
|
27 |
|
28 |
|
29 | function ModelUtils() {
|
30 | }
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 | ModelUtils._allowExtendedOperators = function(options) {
|
39 | const flag = this._getSetting('allowExtendedOperators', options);
|
40 | if (flag != null) return !!flag;
|
41 |
|
42 | return false;
|
43 | };
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 | ModelUtils._getSetting = function(key, options) {
|
54 |
|
55 | let val = options && options[key];
|
56 | if (val !== undefined) return val;
|
57 |
|
58 | const m = this.definition;
|
59 | if (m && m.settings) {
|
60 | val = m.settings[key];
|
61 | if (val !== undefined) {
|
62 | return m.settings[key];
|
63 | }
|
64 |
|
65 | }
|
66 |
|
67 |
|
68 | const ds = this.getDataSource();
|
69 | if (ds && ds.settings) {
|
70 | return ds.settings[key];
|
71 | }
|
72 |
|
73 | return undefined;
|
74 | };
|
75 |
|
76 | const operators = {
|
77 | eq: '=',
|
78 | gt: '>',
|
79 | gte: '>=',
|
80 | lt: '<',
|
81 | lte: '<=',
|
82 | between: 'BETWEEN',
|
83 | inq: 'IN',
|
84 | nin: 'NOT IN',
|
85 | neq: '!=',
|
86 | like: 'LIKE',
|
87 | nlike: 'NOT LIKE',
|
88 | ilike: 'ILIKE',
|
89 | nilike: 'NOT ILIKE',
|
90 | regexp: 'REGEXP',
|
91 | };
|
92 |
|
93 |
|
94 |
|
95 |
|
96 |
|
97 |
|
98 |
|
99 |
|
100 |
|
101 | ModelUtils._normalize = function(filter, options) {
|
102 | if (!filter) {
|
103 | return undefined;
|
104 | }
|
105 | let err = null;
|
106 | if ((typeof filter !== 'object') || Array.isArray(filter)) {
|
107 | err = new Error(g.f('The query filter %j is not an {{object}}', filter));
|
108 | err.statusCode = 400;
|
109 | throw err;
|
110 | }
|
111 | if (filter.limit || filter.skip || filter.offset) {
|
112 | const limit = Number(filter.limit || 100);
|
113 | const offset = Number(filter.skip || filter.offset || 0);
|
114 | if (isNaN(limit) || limit <= 0 || Math.ceil(limit) !== limit) {
|
115 | err = new Error(g.f('The {{limit}} parameter %j is not valid',
|
116 | filter.limit));
|
117 | err.statusCode = 400;
|
118 | throw err;
|
119 | }
|
120 | if (isNaN(offset) || offset < 0 || Math.ceil(offset) !== offset) {
|
121 | err = new Error(g.f('The {{offset/skip}} parameter %j is not valid',
|
122 | filter.skip || filter.offset));
|
123 | err.statusCode = 400;
|
124 | throw err;
|
125 | }
|
126 | filter.limit = limit;
|
127 | filter.offset = offset;
|
128 | filter.skip = offset;
|
129 | }
|
130 |
|
131 | if (filter.order) {
|
132 | let order = filter.order;
|
133 | if (!Array.isArray(order)) {
|
134 | order = [order];
|
135 | }
|
136 | const fields = [];
|
137 | for (let i = 0, m = order.length; i < m; i++) {
|
138 | if (typeof order[i] === 'string') {
|
139 |
|
140 | const tokens = order[i].split(/(?:\s*,\s*)+/);
|
141 | for (let t = 0, n = tokens.length; t < n; t++) {
|
142 | let token = tokens[t];
|
143 | if (token.length === 0) {
|
144 |
|
145 | continue;
|
146 | }
|
147 | const parts = token.split(/\s+/);
|
148 | if (parts.length >= 2) {
|
149 | const dir = parts[1].toUpperCase();
|
150 | if (dir === 'ASC' || dir === 'DESC') {
|
151 | token = parts[0] + ' ' + dir;
|
152 | } else {
|
153 | err = new Error(g.f('The {{order}} %j has invalid direction', token));
|
154 | err.statusCode = 400;
|
155 | throw err;
|
156 | }
|
157 | }
|
158 | fields.push(token);
|
159 | }
|
160 | } else {
|
161 | err = new Error(g.f('The order %j is not valid', order[i]));
|
162 | err.statusCode = 400;
|
163 | throw err;
|
164 | }
|
165 | }
|
166 | if (fields.length === 1 && typeof filter.order === 'string') {
|
167 | filter.order = fields[0];
|
168 | } else {
|
169 | filter.order = fields;
|
170 | }
|
171 | }
|
172 |
|
173 |
|
174 | if (filter.fields) {
|
175 | filter.fields = fieldsToArray(filter.fields,
|
176 | Object.keys(this.definition.properties), this.settings.strict);
|
177 | }
|
178 |
|
179 | filter = this._sanitizeQuery(filter, options);
|
180 | this._coerce(filter.where, options);
|
181 | return filter;
|
182 | };
|
183 |
|
184 | function DateType(arg) {
|
185 | const d = new Date(arg);
|
186 | if (isNaN(d.getTime())) {
|
187 | throw new Error(g.f('Invalid date: %s', arg));
|
188 | }
|
189 | return d;
|
190 | }
|
191 |
|
192 | function BooleanType(arg) {
|
193 | if (typeof arg === 'string') {
|
194 | switch (arg) {
|
195 | case 'true':
|
196 | case '1':
|
197 | return true;
|
198 | case 'false':
|
199 | case '0':
|
200 | return false;
|
201 | }
|
202 | }
|
203 | if (arg == null) {
|
204 | return null;
|
205 | }
|
206 | return Boolean(arg);
|
207 | }
|
208 |
|
209 | function NumberType(val) {
|
210 | const num = Number(val);
|
211 | return !isNaN(num) ? num : val;
|
212 | }
|
213 |
|
214 | function coerceArray(val) {
|
215 | if (Array.isArray(val)) {
|
216 | return val;
|
217 | }
|
218 |
|
219 | if (!isPlainObject(val)) {
|
220 | throw new Error(g.f('Value is not an {{array}} or {{object}} with sequential numeric indices'));
|
221 | }
|
222 |
|
223 |
|
224 | const props = Object.keys(val);
|
225 |
|
226 | if (props.length === 0) {
|
227 | throw new Error(g.f('Value is an empty {{object}}'));
|
228 | }
|
229 |
|
230 | const arrayVal = new Array(props.length);
|
231 | for (let i = 0; i < arrayVal.length; ++i) {
|
232 | if (!val.hasOwnProperty(i)) {
|
233 | throw new Error(g.f('Value is not an {{array}} or {{object}} with sequential numeric indices'));
|
234 | }
|
235 |
|
236 | arrayVal[i] = val[i];
|
237 | }
|
238 |
|
239 | return arrayVal;
|
240 | }
|
241 |
|
242 | function _normalizeAsArray(result) {
|
243 | if (typeof result === 'string') {
|
244 | result = [result];
|
245 | }
|
246 | if (Array.isArray(result)) {
|
247 | return result;
|
248 | } else {
|
249 |
|
250 |
|
251 | const keys = [];
|
252 | for (const k in result) {
|
253 | if (result[k]) keys.push(k);
|
254 | }
|
255 | return keys;
|
256 | }
|
257 | }
|
258 |
|
259 |
|
260 |
|
261 |
|
262 | ModelUtils._getHiddenProperties = function() {
|
263 | const settings = this.definition.settings || {};
|
264 | const result = settings.hiddenProperties || settings.hidden || [];
|
265 | return _normalizeAsArray(result);
|
266 | };
|
267 |
|
268 |
|
269 |
|
270 |
|
271 | ModelUtils._getProtectedProperties = function() {
|
272 | const settings = this.definition.settings || {};
|
273 | const result = settings.protectedProperties || settings.protected || [];
|
274 | return _normalizeAsArray(result);
|
275 | };
|
276 |
|
277 |
|
278 |
|
279 |
|
280 | ModelUtils._getMaxDepthOfQuery = function(options, defaultValue) {
|
281 | options = options || {};
|
282 |
|
283 | let maxDepth = this._getSetting('maxDepthOfQuery', options);
|
284 | if (maxDepth == null) {
|
285 | maxDepth = defaultValue || 32;
|
286 | }
|
287 | return +maxDepth;
|
288 | };
|
289 |
|
290 |
|
291 |
|
292 |
|
293 | ModelUtils._getMaxDepthOfData = function(options, defaultValue) {
|
294 | options = options || {};
|
295 |
|
296 | let maxDepth = this._getSetting('maxDepthOfData', options);
|
297 | if (maxDepth == null) {
|
298 | maxDepth = defaultValue || 64;
|
299 | }
|
300 | return +maxDepth;
|
301 | };
|
302 |
|
303 |
|
304 |
|
305 |
|
306 | ModelUtils._getProhibitHiddenPropertiesInQuery = function(options, defaultValue) {
|
307 | const flag = this._getSetting('prohibitHiddenPropertiesInQuery', options);
|
308 | if (flag == null) return !!defaultValue;
|
309 | return !!flag;
|
310 | };
|
311 |
|
312 |
|
313 |
|
314 |
|
315 | ModelUtils._sanitizeQuery = function(query, options) {
|
316 | options = options || {};
|
317 |
|
318 |
|
319 | const normalizeUndefinedInQuery = this._getSetting('normalizeUndefinedInQuery', options);
|
320 |
|
321 | const prohibitHiddenPropertiesInQuery = this._getProhibitHiddenPropertiesInQuery(options);
|
322 |
|
323 |
|
324 | const maxDepthOfQuery = this._getMaxDepthOfQuery(options);
|
325 |
|
326 | let prohibitedKeys = [];
|
327 |
|
328 | if (prohibitHiddenPropertiesInQuery) {
|
329 | prohibitedKeys = this._getHiddenProperties();
|
330 | if (options.prohibitProtectedPropertiesInQuery) {
|
331 | prohibitedKeys = prohibitedKeys.concat(this._getProtectedProperties());
|
332 | }
|
333 | }
|
334 | return sanitizeQueryOrData(query,
|
335 | Object.assign({
|
336 | maxDepth: maxDepthOfQuery,
|
337 | prohibitedKeys: prohibitedKeys,
|
338 | normalizeUndefinedInQuery: normalizeUndefinedInQuery,
|
339 | }, options));
|
340 | };
|
341 |
|
342 |
|
343 |
|
344 |
|
345 | ModelUtils._sanitizeData = function(data, options) {
|
346 | options = options || {};
|
347 | return sanitizeQueryOrData(data,
|
348 | Object.assign({
|
349 | maxDepth: this._getMaxDepthOfData(options),
|
350 | }, options));
|
351 | };
|
352 |
|
353 |
|
354 |
|
355 |
|
356 |
|
357 |
|
358 |
|
359 |
|
360 |
|
361 |
|
362 | ModelUtils._coerce = function(where, options, modelDef) {
|
363 | const self = this;
|
364 | if (where == null) {
|
365 | return where;
|
366 | }
|
367 | options = options || {};
|
368 |
|
369 | let err;
|
370 | if (typeof where !== 'object' || Array.isArray(where)) {
|
371 | err = new Error(g.f('The where clause %j is not an {{object}}', where));
|
372 | err.statusCode = 400;
|
373 | throw err;
|
374 | }
|
375 | let props;
|
376 | if (modelDef && modelDef.properties) {
|
377 | props = modelDef.properties;
|
378 | } else {
|
379 | props = self.definition.properties;
|
380 | }
|
381 |
|
382 | for (const p in where) {
|
383 |
|
384 | if (p === 'and' || p === 'or' || p === 'nor') {
|
385 | let clauses = where[p];
|
386 | try {
|
387 | clauses = coerceArray(clauses);
|
388 | } catch (e) {
|
389 | err = new Error(g.f('The %s operator has invalid clauses %j: %s', p, clauses, e.message));
|
390 | err.statusCode = 400;
|
391 | throw err;
|
392 | }
|
393 |
|
394 | for (let k = 0; k < clauses.length; k++) {
|
395 | self._coerce(clauses[k], options);
|
396 | }
|
397 |
|
398 | where[p] = clauses;
|
399 |
|
400 | continue;
|
401 | }
|
402 | let DataType = props[p] && props[p].type;
|
403 | if (!DataType) {
|
404 | continue;
|
405 | }
|
406 |
|
407 | if ((Array.isArray(DataType) || DataType === Array) && !isNestedModel(DataType)) {
|
408 | DataType = DataType[0];
|
409 | }
|
410 | if (DataType === Date) {
|
411 | DataType = DateType;
|
412 | } else if (DataType === Boolean) {
|
413 | DataType = BooleanType;
|
414 | } else if (DataType === Number) {
|
415 |
|
416 |
|
417 |
|
418 |
|
419 |
|
420 | DataType = NumberType;
|
421 | }
|
422 |
|
423 | if (!DataType) {
|
424 | continue;
|
425 | }
|
426 |
|
427 | if (DataType === geo.GeoPoint) {
|
428 |
|
429 |
|
430 |
|
431 |
|
432 |
|
433 | continue;
|
434 | }
|
435 |
|
436 | let val = where[p];
|
437 | if (val === null || val === undefined) {
|
438 | continue;
|
439 | }
|
440 |
|
441 | let operator = null;
|
442 | const exp = val;
|
443 | if (val.constructor === Object) {
|
444 | for (const op in operators) {
|
445 | if (op in val) {
|
446 | val = val[op];
|
447 | operator = op;
|
448 | switch (operator) {
|
449 | case 'inq':
|
450 | case 'nin':
|
451 | case 'between':
|
452 | try {
|
453 | val = coerceArray(val);
|
454 | } catch (e) {
|
455 | err = new Error(g.f('The %s property has invalid clause %j: %s', p, where[p], e));
|
456 | err.statusCode = 400;
|
457 | throw err;
|
458 | }
|
459 |
|
460 | if (operator === 'between' && val.length !== 2) {
|
461 | err = new Error(g.f(
|
462 | 'The %s property has invalid clause %j: Expected precisely 2 values, received %d',
|
463 | p,
|
464 | where[p],
|
465 | val.length,
|
466 | ));
|
467 | err.statusCode = 400;
|
468 | throw err;
|
469 | }
|
470 | break;
|
471 | case 'like':
|
472 | case 'nlike':
|
473 | case 'ilike':
|
474 | case 'nilike':
|
475 | if (!(typeof val === 'string' || val instanceof RegExp)) {
|
476 | err = new Error(g.f(
|
477 | 'The %s property has invalid clause %j: Expected a string or RegExp',
|
478 | p,
|
479 | where[p],
|
480 | ));
|
481 | err.statusCode = 400;
|
482 | throw err;
|
483 | }
|
484 | break;
|
485 | case 'regexp':
|
486 | val = toRegExp(val);
|
487 | if (val instanceof Error) {
|
488 | val.statusCode = 400;
|
489 | throw val;
|
490 | }
|
491 | break;
|
492 | }
|
493 | break;
|
494 | }
|
495 | }
|
496 | }
|
497 |
|
498 | try {
|
499 |
|
500 | val = coerceArray(val);
|
501 | } catch (e) {
|
502 |
|
503 | }
|
504 |
|
505 | const allowExtendedOperators = self._allowExtendedOperators(options);
|
506 |
|
507 | if (Array.isArray(val) && !isNestedModel(DataType)) {
|
508 | for (let i = 0; i < val.length; i++) {
|
509 | if (val[i] !== null && val[i] !== undefined) {
|
510 | if (!(val[i] instanceof RegExp)) {
|
511 | val[i] = isClass(DataType) ? new DataType(val[i]) : DataType(val[i]);
|
512 | }
|
513 | }
|
514 | }
|
515 | } else {
|
516 | if (val != null) {
|
517 | if (operator === null && val instanceof RegExp) {
|
518 |
|
519 | operator = 'regexp';
|
520 | } else if (operator === 'regexp' && val instanceof RegExp) {
|
521 |
|
522 | } else if ((operator === 'like' || operator === 'nlike' ||
|
523 | operator === 'ilike' || operator === 'nilike') && val instanceof RegExp) {
|
524 |
|
525 | } else if (allowExtendedOperators && typeof val === 'object') {
|
526 |
|
527 | } else {
|
528 | if (!allowExtendedOperators) {
|
529 | const extendedOperators = Object.keys(val).filter(function(k) {
|
530 | return k[0] === '$';
|
531 | });
|
532 | if (extendedOperators.length) {
|
533 | const msg = g.f('Operators "' + extendedOperators.join(', ') + '" are not allowed in query');
|
534 | const err = new Error(msg);
|
535 | err.code = 'OPERATOR_NOT_ALLOWED_IN_QUERY';
|
536 | err.statusCode = 400;
|
537 | err.details = {
|
538 | operators: extendedOperators,
|
539 | where: where,
|
540 | };
|
541 | throw err;
|
542 | }
|
543 | }
|
544 | if (isNestedModel(DataType)) {
|
545 | if (Array.isArray(DataType) && Array.isArray(val)) {
|
546 | if (val === null || val === undefined) continue;
|
547 | for (const it of val) {
|
548 | self._coerce(it, options, DataType[0].definition);
|
549 | }
|
550 | } else {
|
551 | self._coerce(val, options, DataType.definition);
|
552 | }
|
553 | continue;
|
554 | } else {
|
555 | val = isClass(DataType) ? new DataType(val) : DataType(val);
|
556 | }
|
557 | }
|
558 | }
|
559 | }
|
560 |
|
561 | if (operator && operator !== 'eq') {
|
562 | const value = {};
|
563 | value[operator] = val;
|
564 | if (exp.options) {
|
565 |
|
566 | value.options = exp.options;
|
567 | }
|
568 | val = value;
|
569 | }
|
570 | where[p] = val;
|
571 | }
|
572 | return where;
|
573 | };
|
574 |
|
575 |
|
576 |
|
577 |
|
578 |
|
579 |
|
580 |
|
581 | function isNestedModel(propType) {
|
582 | if (!propType) return false;
|
583 | if (Array.isArray(propType)) return isNestedModel(propType[0]);
|
584 | return propType.hasOwnProperty('definition') && propType.definition.hasOwnProperty('properties');
|
585 | }
|
586 |
|