You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

671 lines
14 KiB

  1. /*!
  2. * Module dependencies.
  3. */
  4. var ReadPref = require('mongodb').ReadPreference
  5. , ObjectId = require('./types/objectid')
  6. , cloneRegExp = require('regexp-clone')
  7. , sliced = require('sliced')
  8. , mpath = require('mpath')
  9. , ms = require('ms')
  10. , MongooseBuffer
  11. , MongooseArray
  12. , Document
  13. /*!
  14. * Produces a collection name from model `name`.
  15. *
  16. * @param {String} name a model name
  17. * @return {String} a collection name
  18. * @api private
  19. */
  20. exports.toCollectionName = function (name) {
  21. if ('system.profile' === name) return name;
  22. if ('system.indexes' === name) return name;
  23. return pluralize(name.toLowerCase());
  24. };
  25. /**
  26. * Pluralization rules.
  27. *
  28. * These rules are applied while processing the argument to `toCollectionName`.
  29. *
  30. * @deprecated remove in 4.x gh-1350
  31. */
  32. exports.pluralization = [
  33. [/(m)an$/gi, '$1en'],
  34. [/(pe)rson$/gi, '$1ople'],
  35. [/(child)$/gi, '$1ren'],
  36. [/^(ox)$/gi, '$1en'],
  37. [/(ax|test)is$/gi, '$1es'],
  38. [/(octop|vir)us$/gi, '$1i'],
  39. [/(alias|status)$/gi, '$1es'],
  40. [/(bu)s$/gi, '$1ses'],
  41. [/(buffal|tomat|potat)o$/gi, '$1oes'],
  42. [/([ti])um$/gi, '$1a'],
  43. [/sis$/gi, 'ses'],
  44. [/(?:([^f])fe|([lr])f)$/gi, '$1$2ves'],
  45. [/(hive)$/gi, '$1s'],
  46. [/([^aeiouy]|qu)y$/gi, '$1ies'],
  47. [/(x|ch|ss|sh)$/gi, '$1es'],
  48. [/(matr|vert|ind)ix|ex$/gi, '$1ices'],
  49. [/([m|l])ouse$/gi, '$1ice'],
  50. [/(quiz)$/gi, '$1zes'],
  51. [/s$/gi, 's'],
  52. [/$/gi, 's']
  53. ];
  54. var rules = exports.pluralization;
  55. /**
  56. * Uncountable words.
  57. *
  58. * These words are applied while processing the argument to `toCollectionName`.
  59. * @api public
  60. */
  61. exports.uncountables = [
  62. 'advice',
  63. 'energy',
  64. 'excretion',
  65. 'digestion',
  66. 'cooperation',
  67. 'health',
  68. 'justice',
  69. 'labour',
  70. 'machinery',
  71. 'equipment',
  72. 'information',
  73. 'pollution',
  74. 'sewage',
  75. 'paper',
  76. 'money',
  77. 'species',
  78. 'series',
  79. 'rain',
  80. 'rice',
  81. 'fish',
  82. 'sheep',
  83. 'moose',
  84. 'deer',
  85. 'news',
  86. 'expertise',
  87. 'status',
  88. 'media'
  89. ];
  90. var uncountables = exports.uncountables;
  91. /*!
  92. * Pluralize function.
  93. *
  94. * @author TJ Holowaychuk (extracted from _ext.js_)
  95. * @param {String} string to pluralize
  96. * @api private
  97. */
  98. function pluralize (str) {
  99. var rule, found;
  100. if (!~uncountables.indexOf(str.toLowerCase())){
  101. found = rules.filter(function(rule){
  102. return str.match(rule[0]);
  103. });
  104. if (found[0]) return str.replace(found[0][0], found[0][1]);
  105. }
  106. return str;
  107. };
  108. /*!
  109. * Determines if `a` and `b` are deep equal.
  110. *
  111. * Modified from node/lib/assert.js
  112. *
  113. * @param {any} a a value to compare to `b`
  114. * @param {any} b a value to compare to `a`
  115. * @return {Boolean}
  116. * @api private
  117. */
  118. exports.deepEqual = function deepEqual (a, b) {
  119. if (a === b) return true;
  120. if (a instanceof Date && b instanceof Date)
  121. return a.getTime() === b.getTime();
  122. if (a instanceof ObjectId && b instanceof ObjectId) {
  123. return a.toString() === b.toString();
  124. }
  125. if (a instanceof RegExp && b instanceof RegExp) {
  126. return a.source == b.source &&
  127. a.ignoreCase == b.ignoreCase &&
  128. a.multiline == b.multiline &&
  129. a.global == b.global;
  130. }
  131. if (typeof a !== 'object' && typeof b !== 'object')
  132. return a == b;
  133. if (a === null || b === null || a === undefined || b === undefined)
  134. return false
  135. if (a.prototype !== b.prototype) return false;
  136. // Handle MongooseNumbers
  137. if (a instanceof Number && b instanceof Number) {
  138. return a.valueOf() === b.valueOf();
  139. }
  140. if (Buffer.isBuffer(a)) {
  141. if (!Buffer.isBuffer(b)) return false;
  142. if (a.length !== b.length) return false;
  143. for (var i = 0, len = a.length; i < len; ++i) {
  144. if (a[i] !== b[i]) return false;
  145. }
  146. return true;
  147. }
  148. if (isMongooseObject(a)) a = a.toObject();
  149. if (isMongooseObject(b)) b = b.toObject();
  150. try {
  151. var ka = Object.keys(a),
  152. kb = Object.keys(b),
  153. key, i;
  154. } catch (e) {//happens when one is a string literal and the other isn't
  155. return false;
  156. }
  157. // having the same number of owned properties (keys incorporates
  158. // hasOwnProperty)
  159. if (ka.length != kb.length)
  160. return false;
  161. //the same set of keys (although not necessarily the same order),
  162. ka.sort();
  163. kb.sort();
  164. //~~~cheap key test
  165. for (i = ka.length - 1; i >= 0; i--) {
  166. if (ka[i] != kb[i])
  167. return false;
  168. }
  169. //equivalent values for every corresponding key, and
  170. //~~~possibly expensive deep test
  171. for (i = ka.length - 1; i >= 0; i--) {
  172. key = ka[i];
  173. if (!deepEqual(a[key], b[key])) return false;
  174. }
  175. return true;
  176. };
  177. /*!
  178. * Object clone with Mongoose natives support.
  179. *
  180. * If options.minimize is true, creates a minimal data object. Empty objects and undefined values will not be cloned. This makes the data payload sent to MongoDB as small as possible.
  181. *
  182. * Functions are never cloned.
  183. *
  184. * @param {Object} obj the object to clone
  185. * @param {Object} options
  186. * @return {Object} the cloned object
  187. * @api private
  188. */
  189. exports.clone = function clone (obj, options) {
  190. if (obj === undefined || obj === null)
  191. return obj;
  192. if (Array.isArray(obj))
  193. return cloneArray(obj, options);
  194. if (isMongooseObject(obj)) {
  195. if (options && options.json && 'function' === typeof obj.toJSON) {
  196. return obj.toJSON(options);
  197. } else {
  198. return obj.toObject(options);
  199. }
  200. }
  201. if (obj.constructor) {
  202. switch (obj.constructor.name) {
  203. case 'Object':
  204. return cloneObject(obj, options);
  205. case 'Date':
  206. return new obj.constructor(+obj);
  207. case 'RegExp':
  208. return cloneRegExp(obj);
  209. default:
  210. // ignore
  211. break;
  212. }
  213. }
  214. if (obj instanceof ObjectId)
  215. return new ObjectId(obj.id);
  216. if (!obj.constructor && exports.isObject(obj)) {
  217. // object created with Object.create(null)
  218. return cloneObject(obj, options);
  219. }
  220. if (obj.valueOf)
  221. return obj.valueOf();
  222. };
  223. var clone = exports.clone;
  224. /*!
  225. * ignore
  226. */
  227. function cloneObject (obj, options) {
  228. var retainKeyOrder = options && options.retainKeyOrder
  229. , minimize = options && options.minimize
  230. , ret = {}
  231. , hasKeys
  232. , keys
  233. , val
  234. , k
  235. , i
  236. if (retainKeyOrder) {
  237. for (k in obj) {
  238. val = clone(obj[k], options);
  239. if (!minimize || ('undefined' !== typeof val)) {
  240. hasKeys || (hasKeys = true);
  241. ret[k] = val;
  242. }
  243. }
  244. } else {
  245. // faster
  246. keys = Object.keys(obj);
  247. i = keys.length;
  248. while (i--) {
  249. k = keys[i];
  250. val = clone(obj[k], options);
  251. if (!minimize || ('undefined' !== typeof val)) {
  252. if (!hasKeys) hasKeys = true;
  253. ret[k] = val;
  254. }
  255. }
  256. }
  257. return minimize
  258. ? hasKeys && ret
  259. : ret;
  260. };
  261. function cloneArray (arr, options) {
  262. var ret = [];
  263. for (var i = 0, l = arr.length; i < l; i++)
  264. ret.push(clone(arr[i], options));
  265. return ret;
  266. };
  267. /*!
  268. * Shallow copies defaults into options.
  269. *
  270. * @param {Object} defaults
  271. * @param {Object} options
  272. * @return {Object} the merged object
  273. * @api private
  274. */
  275. exports.options = function (defaults, options) {
  276. var keys = Object.keys(defaults)
  277. , i = keys.length
  278. , k ;
  279. options = options || {};
  280. while (i--) {
  281. k = keys[i];
  282. if (!(k in options)) {
  283. options[k] = defaults[k];
  284. }
  285. }
  286. return options;
  287. };
  288. /*!
  289. * Generates a random string
  290. *
  291. * @api private
  292. */
  293. exports.random = function () {
  294. return Math.random().toString().substr(3);
  295. };
  296. /*!
  297. * Merges `from` into `to` without overwriting existing properties.
  298. *
  299. * @param {Object} to
  300. * @param {Object} from
  301. * @api private
  302. */
  303. exports.merge = function merge (to, from) {
  304. var keys = Object.keys(from)
  305. , i = keys.length
  306. , key
  307. while (i--) {
  308. key = keys[i];
  309. if ('undefined' === typeof to[key]) {
  310. to[key] = from[key];
  311. } else {
  312. if (exports.isObject(from[key])) {
  313. merge(to[key], from[key]);
  314. } else {
  315. to[key] = from[key];
  316. }
  317. }
  318. }
  319. };
  320. /*!
  321. * toString helper
  322. */
  323. var toString = Object.prototype.toString;
  324. /*!
  325. * Determines if `arg` is an object.
  326. *
  327. * @param {Object|Array|String|Function|RegExp|any} arg
  328. * @api private
  329. * @return {Boolean}
  330. */
  331. exports.isObject = function (arg) {
  332. return '[object Object]' == toString.call(arg);
  333. }
  334. /*!
  335. * A faster Array.prototype.slice.call(arguments) alternative
  336. * @api private
  337. */
  338. exports.args = sliced;
  339. /*!
  340. * process.nextTick helper.
  341. *
  342. * Wraps `callback` in a try/catch + nextTick.
  343. *
  344. * node-mongodb-native has a habit of state corruption when an error is immediately thrown from within a collection callback.
  345. *
  346. * @param {Function} callback
  347. * @api private
  348. */
  349. exports.tick = function tick (callback) {
  350. if ('function' !== typeof callback) return;
  351. return function () {
  352. try {
  353. callback.apply(this, arguments);
  354. } catch (err) {
  355. // only nextTick on err to get out of
  356. // the event loop and avoid state corruption.
  357. process.nextTick(function () {
  358. throw err;
  359. });
  360. }
  361. }
  362. }
  363. /*!
  364. * Returns if `v` is a mongoose object that has a `toObject()` method we can use.
  365. *
  366. * This is for compatibility with libs like Date.js which do foolish things to Natives.
  367. *
  368. * @param {any} v
  369. * @api private
  370. */
  371. exports.isMongooseObject = function (v) {
  372. Document || (Document = require('./document'));
  373. MongooseArray || (MongooseArray = require('./types').Array);
  374. MongooseBuffer || (MongooseBuffer = require('./types').Buffer);
  375. return v instanceof Document ||
  376. v instanceof MongooseArray ||
  377. v instanceof MongooseBuffer
  378. }
  379. var isMongooseObject = exports.isMongooseObject;
  380. /*!
  381. * Converts `expires` options of index objects to `expiresAfterSeconds` options for MongoDB.
  382. *
  383. * @param {Object} object
  384. * @api private
  385. */
  386. exports.expires = function expires (object) {
  387. if (!(object && 'Object' == object.constructor.name)) return;
  388. if (!('expires' in object)) return;
  389. var when;
  390. if ('string' != typeof object.expires) {
  391. when = object.expires;
  392. } else {
  393. when = Math.round(ms(object.expires) / 1000);
  394. }
  395. object.expireAfterSeconds = when;
  396. delete object.expires;
  397. }
  398. /*!
  399. * Converts arguments to ReadPrefs the driver
  400. * can understand.
  401. *
  402. * @TODO move this into the driver layer
  403. * @param {String|Array} pref
  404. * @param {Array} [tags]
  405. */
  406. exports.readPref = function readPref (pref, tags) {
  407. if (Array.isArray(pref)) {
  408. tags = pref[1];
  409. pref = pref[0];
  410. }
  411. switch (pref) {
  412. case 'p':
  413. pref = 'primary';
  414. break;
  415. case 'pp':
  416. pref = 'primaryPreferred';
  417. break;
  418. case 's':
  419. pref = 'secondary';
  420. break;
  421. case 'sp':
  422. pref = 'secondaryPreferred';
  423. break;
  424. case 'n':
  425. pref = 'nearest';
  426. break;
  427. }
  428. return new ReadPref(pref, tags);
  429. }
  430. /*!
  431. * Populate options constructor
  432. */
  433. function PopulateOptions (path, select, match, options, model) {
  434. this.path = path;
  435. this.match = match;
  436. this.select = select;
  437. this.options = options;
  438. this.model = model;
  439. this._docs = {};
  440. }
  441. // make it compatible with utils.clone
  442. PopulateOptions.prototype.constructor = Object;
  443. // expose
  444. exports.PopulateOptions = PopulateOptions;
  445. /*!
  446. * populate helper
  447. */
  448. exports.populate = function populate (path, select, model, match, options) {
  449. // The order of select/conditions args is opposite Model.find but
  450. // necessary to keep backward compatibility (select could be
  451. // an array, string, or object literal).
  452. // might have passed an object specifying all arguments
  453. if (1 === arguments.length) {
  454. if (path instanceof PopulateOptions) {
  455. return [path];
  456. }
  457. if (Array.isArray(path)) {
  458. return path.map(function(o){
  459. return exports.populate(o)[0];
  460. });
  461. }
  462. if (exports.isObject(path)) {
  463. match = path.match;
  464. options = path.options;
  465. select = path.select;
  466. model = path.model;
  467. path = path.path;
  468. }
  469. } else if ('string' !== typeof model) {
  470. options = match;
  471. match = model;
  472. model = undefined;
  473. }
  474. if ('string' != typeof path) {
  475. throw new TypeError('utils.populate: invalid path. Expected string. Got typeof `' + typeof path + '`');
  476. }
  477. var ret = [];
  478. var paths = path.split(' ');
  479. for (var i = 0; i < paths.length; ++i) {
  480. ret.push(new PopulateOptions(paths[i], select, match, options, model));
  481. }
  482. return ret;
  483. }
  484. /*!
  485. * Return the value of `obj` at the given `path`.
  486. *
  487. * @param {String} path
  488. * @param {Object} obj
  489. */
  490. exports.getValue = function (path, obj, map) {
  491. return mpath.get(path, obj, '_doc', map);
  492. }
  493. /*!
  494. * Sets the value of `obj` at the given `path`.
  495. *
  496. * @param {String} path
  497. * @param {Anything} val
  498. * @param {Object} obj
  499. */
  500. exports.setValue = function (path, val, obj, map) {
  501. mpath.set(path, val, obj, '_doc', map);
  502. }
  503. /*!
  504. * Returns an array of values from object `o`.
  505. *
  506. * @param {Object} o
  507. * @return {Array}
  508. * @private
  509. */
  510. exports.object = {};
  511. exports.object.vals = function vals (o) {
  512. var keys = Object.keys(o)
  513. , i = keys.length
  514. , ret = [];
  515. while (i--) {
  516. ret.push(o[keys[i]]);
  517. }
  518. return ret;
  519. }
  520. /*!
  521. * @see exports.options
  522. */
  523. exports.object.shallowCopy = exports.options;
  524. /*!
  525. * Safer helper for hasOwnProperty checks
  526. *
  527. * @param {Object} obj
  528. * @param {String} prop
  529. */
  530. exports.object.hasOwnProperty = function (obj, prop) {
  531. return Object.prototype.hasOwnProperty.call(obj, prop);
  532. }
  533. /*!
  534. * Determine if `val` is null or undefined
  535. *
  536. * @return {Boolean}
  537. */
  538. exports.isNullOrUndefined = function (val) {
  539. return null == val
  540. }
  541. /*!
  542. * ignore
  543. */
  544. exports.array = {};
  545. /*!
  546. * Flattens an array.
  547. *
  548. * [ 1, [ 2, 3, [4] ]] -> [1,2,3,4]
  549. *
  550. * @param {Array} arr
  551. * @param {Function} [filter] If passed, will be invoked with each item in the array. If `filter` returns a falsey value, the item will not be included in the results.
  552. * @return {Array}
  553. * @private
  554. */
  555. exports.array.flatten = function flatten (arr, filter, ret) {
  556. ret || (ret = []);
  557. arr.forEach(function (item) {
  558. if (Array.isArray(item)) {
  559. flatten(item, filter, ret);
  560. } else {
  561. if (!filter || filter(item)) {
  562. ret.push(item);
  563. }
  564. }
  565. });
  566. return ret;
  567. }