objectid.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. 'use strict';
  2. const Buffer = require('buffer').Buffer;
  3. let randomBytes = require('./parser/utils').randomBytes;
  4. const deprecate = require('util').deprecate;
  5. // constants
  6. const PROCESS_UNIQUE = randomBytes(5);
  7. // Regular expression that checks for hex value
  8. const checkForHexRegExp = new RegExp('^[0-9a-fA-F]{24}$');
  9. let hasBufferType = false;
  10. // Check if buffer exists
  11. try {
  12. if (Buffer && Buffer.from) hasBufferType = true;
  13. } catch (err) {
  14. hasBufferType = false;
  15. }
  16. // Precomputed hex table enables speedy hex string conversion
  17. const hexTable = [];
  18. for (let i = 0; i < 256; i++) {
  19. hexTable[i] = (i <= 15 ? '0' : '') + i.toString(16);
  20. }
  21. // Lookup tables
  22. const decodeLookup = [];
  23. let i = 0;
  24. while (i < 10) decodeLookup[0x30 + i] = i++;
  25. while (i < 16) decodeLookup[0x41 - 10 + i] = decodeLookup[0x61 - 10 + i] = i++;
  26. const _Buffer = Buffer;
  27. function convertToHex(bytes) {
  28. return bytes.toString('hex');
  29. }
  30. function makeObjectIdError(invalidString, index) {
  31. const invalidCharacter = invalidString[index];
  32. return new TypeError(
  33. `ObjectId string "${invalidString}" contains invalid character "${invalidCharacter}" with character code (${invalidString.charCodeAt(
  34. index
  35. )}). All character codes for a non-hex string must be less than 256.`
  36. );
  37. }
  38. /**
  39. * A class representation of the BSON ObjectId type.
  40. */
  41. class ObjectId {
  42. /**
  43. * Create an ObjectId type
  44. *
  45. * @param {(string|number)} id Can be a 24 byte hex string, 12 byte binary string or a Number.
  46. * @property {number} generationTime The generation time of this ObjectId instance
  47. * @return {ObjectId} instance of ObjectId.
  48. */
  49. constructor(id) {
  50. // Duck-typing to support ObjectId from different npm packages
  51. if (id instanceof ObjectId) return id;
  52. // The most common usecase (blank id, new objectId instance)
  53. if (id == null || typeof id === 'number') {
  54. // Generate a new id
  55. this.id = ObjectId.generate(id);
  56. // If we are caching the hex string
  57. if (ObjectId.cacheHexString) this.__id = this.toString('hex');
  58. // Return the object
  59. return;
  60. }
  61. // Check if the passed in id is valid
  62. const valid = ObjectId.isValid(id);
  63. // Throw an error if it's not a valid setup
  64. if (!valid && id != null) {
  65. throw new TypeError(
  66. 'Argument passed in must be a single String of 12 bytes or a string of 24 hex characters'
  67. );
  68. } else if (valid && typeof id === 'string' && id.length === 24 && hasBufferType) {
  69. return new ObjectId(Buffer.from(id, 'hex'));
  70. } else if (valid && typeof id === 'string' && id.length === 24) {
  71. return ObjectId.createFromHexString(id);
  72. } else if (id != null && id.length === 12) {
  73. // assume 12 byte string
  74. this.id = id;
  75. } else if (id != null && id.toHexString) {
  76. // Duck-typing to support ObjectId from different npm packages
  77. return id;
  78. } else {
  79. throw new TypeError(
  80. 'Argument passed in must be a single String of 12 bytes or a string of 24 hex characters'
  81. );
  82. }
  83. if (ObjectId.cacheHexString) this.__id = this.toString('hex');
  84. }
  85. /**
  86. * Return the ObjectId id as a 24 byte hex string representation
  87. *
  88. * @method
  89. * @return {string} return the 24 byte hex string representation.
  90. */
  91. toHexString() {
  92. if (ObjectId.cacheHexString && this.__id) return this.__id;
  93. let hexString = '';
  94. if (!this.id || !this.id.length) {
  95. throw new TypeError(
  96. 'invalid ObjectId, ObjectId.id must be either a string or a Buffer, but is [' +
  97. JSON.stringify(this.id) +
  98. ']'
  99. );
  100. }
  101. if (this.id instanceof _Buffer) {
  102. hexString = convertToHex(this.id);
  103. if (ObjectId.cacheHexString) this.__id = hexString;
  104. return hexString;
  105. }
  106. for (let i = 0; i < this.id.length; i++) {
  107. const hexChar = hexTable[this.id.charCodeAt(i)];
  108. if (typeof hexChar !== 'string') {
  109. throw makeObjectIdError(this.id, i);
  110. }
  111. hexString += hexChar;
  112. }
  113. if (ObjectId.cacheHexString) this.__id = hexString;
  114. return hexString;
  115. }
  116. /**
  117. * Update the ObjectId index used in generating new ObjectId's on the driver
  118. *
  119. * @method
  120. * @return {number} returns next index value.
  121. * @ignore
  122. */
  123. static getInc() {
  124. return (ObjectId.index = (ObjectId.index + 1) % 0xffffff);
  125. }
  126. /**
  127. * Generate a 12 byte id buffer used in ObjectId's
  128. *
  129. * @method
  130. * @param {number} [time] optional parameter allowing to pass in a second based timestamp.
  131. * @return {Buffer} return the 12 byte id buffer string.
  132. */
  133. static generate(time) {
  134. if ('number' !== typeof time) {
  135. time = ~~(Date.now() / 1000);
  136. }
  137. const inc = ObjectId.getInc();
  138. const buffer = Buffer.alloc(12);
  139. // 4-byte timestamp
  140. buffer[3] = time & 0xff;
  141. buffer[2] = (time >> 8) & 0xff;
  142. buffer[1] = (time >> 16) & 0xff;
  143. buffer[0] = (time >> 24) & 0xff;
  144. // 5-byte process unique
  145. buffer[4] = PROCESS_UNIQUE[0];
  146. buffer[5] = PROCESS_UNIQUE[1];
  147. buffer[6] = PROCESS_UNIQUE[2];
  148. buffer[7] = PROCESS_UNIQUE[3];
  149. buffer[8] = PROCESS_UNIQUE[4];
  150. // 3-byte counter
  151. buffer[11] = inc & 0xff;
  152. buffer[10] = (inc >> 8) & 0xff;
  153. buffer[9] = (inc >> 16) & 0xff;
  154. return buffer;
  155. }
  156. /**
  157. * Converts the id into a 24 byte hex string for printing
  158. *
  159. * @param {String} format The Buffer toString format parameter.
  160. * @return {String} return the 24 byte hex string representation.
  161. * @ignore
  162. */
  163. toString(format) {
  164. // Is the id a buffer then use the buffer toString method to return the format
  165. if (this.id && this.id.copy) {
  166. return this.id.toString(typeof format === 'string' ? format : 'hex');
  167. }
  168. return this.toHexString();
  169. }
  170. /**
  171. * Converts to its JSON representation.
  172. *
  173. * @return {String} return the 24 byte hex string representation.
  174. * @ignore
  175. */
  176. toJSON() {
  177. return this.toHexString();
  178. }
  179. /**
  180. * Compares the equality of this ObjectId with `otherID`.
  181. *
  182. * @method
  183. * @param {object} otherID ObjectId instance to compare against.
  184. * @return {boolean} the result of comparing two ObjectId's
  185. */
  186. equals(otherId) {
  187. if (otherId instanceof ObjectId) {
  188. return this.toString() === otherId.toString();
  189. }
  190. if (
  191. typeof otherId === 'string' &&
  192. ObjectId.isValid(otherId) &&
  193. otherId.length === 12 &&
  194. this.id instanceof _Buffer
  195. ) {
  196. return otherId === this.id.toString('binary');
  197. }
  198. if (typeof otherId === 'string' && ObjectId.isValid(otherId) && otherId.length === 24) {
  199. return otherId.toLowerCase() === this.toHexString();
  200. }
  201. if (typeof otherId === 'string' && ObjectId.isValid(otherId) && otherId.length === 12) {
  202. return otherId === this.id;
  203. }
  204. if (otherId != null && (otherId instanceof ObjectId || otherId.toHexString)) {
  205. return otherId.toHexString() === this.toHexString();
  206. }
  207. return false;
  208. }
  209. /**
  210. * Returns the generation date (accurate up to the second) that this ID was generated.
  211. *
  212. * @method
  213. * @return {date} the generation date
  214. */
  215. getTimestamp() {
  216. const timestamp = new Date();
  217. const time = this.id[3] | (this.id[2] << 8) | (this.id[1] << 16) | (this.id[0] << 24);
  218. timestamp.setTime(Math.floor(time) * 1000);
  219. return timestamp;
  220. }
  221. /**
  222. * @ignore
  223. */
  224. static createPk() {
  225. return new ObjectId();
  226. }
  227. /**
  228. * Creates an ObjectId from a second based number, with the rest of the ObjectId zeroed out. Used for comparisons or sorting the ObjectId.
  229. *
  230. * @method
  231. * @param {number} time an integer number representing a number of seconds.
  232. * @return {ObjectId} return the created ObjectId
  233. */
  234. static createFromTime(time) {
  235. const buffer = Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
  236. // Encode time into first 4 bytes
  237. buffer[3] = time & 0xff;
  238. buffer[2] = (time >> 8) & 0xff;
  239. buffer[1] = (time >> 16) & 0xff;
  240. buffer[0] = (time >> 24) & 0xff;
  241. // Return the new objectId
  242. return new ObjectId(buffer);
  243. }
  244. /**
  245. * Creates an ObjectId from a hex string representation of an ObjectId.
  246. *
  247. * @method
  248. * @param {string} hexString create a ObjectId from a passed in 24 byte hexstring.
  249. * @return {ObjectId} return the created ObjectId
  250. */
  251. static createFromHexString(string) {
  252. // Throw an error if it's not a valid setup
  253. if (typeof string === 'undefined' || (string != null && string.length !== 24)) {
  254. throw new TypeError(
  255. 'Argument passed in must be a single String of 12 bytes or a string of 24 hex characters'
  256. );
  257. }
  258. // Use Buffer.from method if available
  259. if (hasBufferType) return new ObjectId(Buffer.from(string, 'hex'));
  260. // Calculate lengths
  261. const array = new _Buffer(12);
  262. let n = 0;
  263. let i = 0;
  264. while (i < 24) {
  265. array[n++] =
  266. (decodeLookup[string.charCodeAt(i++)] << 4) | decodeLookup[string.charCodeAt(i++)];
  267. }
  268. return new ObjectId(array);
  269. }
  270. /**
  271. * Checks if a value is a valid bson ObjectId
  272. *
  273. * @method
  274. * @return {boolean} return true if the value is a valid bson ObjectId, return false otherwise.
  275. */
  276. static isValid(id) {
  277. if (id == null) return false;
  278. if (typeof id === 'number') {
  279. return true;
  280. }
  281. if (typeof id === 'string') {
  282. return id.length === 12 || (id.length === 24 && checkForHexRegExp.test(id));
  283. }
  284. if (id instanceof ObjectId) {
  285. return true;
  286. }
  287. if (id instanceof _Buffer && id.length === 12) {
  288. return true;
  289. }
  290. // Duck-Typing detection of ObjectId like objects
  291. if (id.toHexString) {
  292. return id.id.length === 12 || (id.id.length === 24 && checkForHexRegExp.test(id.id));
  293. }
  294. return false;
  295. }
  296. /**
  297. * @ignore
  298. */
  299. toExtendedJSON() {
  300. if (this.toHexString) return { $oid: this.toHexString() };
  301. return { $oid: this.toString('hex') };
  302. }
  303. /**
  304. * @ignore
  305. */
  306. static fromExtendedJSON(doc) {
  307. return new ObjectId(doc.$oid);
  308. }
  309. }
  310. // Deprecated methods
  311. ObjectId.get_inc = deprecate(
  312. () => ObjectId.getInc(),
  313. 'Please use the static `ObjectId.getInc()` instead'
  314. );
  315. ObjectId.prototype.get_inc = deprecate(
  316. () => ObjectId.getInc(),
  317. 'Please use the static `ObjectId.getInc()` instead'
  318. );
  319. ObjectId.prototype.getInc = deprecate(
  320. () => ObjectId.getInc(),
  321. 'Please use the static `ObjectId.getInc()` instead'
  322. );
  323. ObjectId.prototype.generate = deprecate(
  324. time => ObjectId.generate(time),
  325. 'Please use the static `ObjectId.generate(time)` instead'
  326. );
  327. /**
  328. * @ignore
  329. */
  330. Object.defineProperty(ObjectId.prototype, 'generationTime', {
  331. enumerable: true,
  332. get: function() {
  333. return this.id[3] | (this.id[2] << 8) | (this.id[1] << 16) | (this.id[0] << 24);
  334. },
  335. set: function(value) {
  336. // Encode time into first 4 bytes
  337. this.id[3] = value & 0xff;
  338. this.id[2] = (value >> 8) & 0xff;
  339. this.id[1] = (value >> 16) & 0xff;
  340. this.id[0] = (value >> 24) & 0xff;
  341. }
  342. });
  343. /**
  344. * Converts to a string representation of this Id.
  345. *
  346. * @return {String} return the 24 byte hex string representation.
  347. * @ignore
  348. */
  349. ObjectId.prototype.inspect = ObjectId.prototype.toString;
  350. /**
  351. * @ignore
  352. */
  353. ObjectId.index = ~~(Math.random() * 0xffffff);
  354. Object.defineProperty(ObjectId.prototype, '_bsontype', { value: 'ObjectId' });
  355. module.exports = ObjectId;