objectid.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. import { BSONValue } from './bson_value';
  2. import { BSONError } from './error';
  3. import { isUint8Array } from './parser/utils';
  4. import { BSONDataView, ByteUtils } from './utils/byte_utils';
  5. // Regular expression that checks for hex value
  6. const checkForHexRegExp = new RegExp('^[0-9a-fA-F]{24}$');
  7. // Unique sequence for the current process (initialized on first use)
  8. let PROCESS_UNIQUE: Uint8Array | null = null;
  9. /** @public */
  10. export interface ObjectIdLike {
  11. id: string | Uint8Array;
  12. __id?: string;
  13. toHexString(): string;
  14. }
  15. /** @public */
  16. export interface ObjectIdExtended {
  17. $oid: string;
  18. }
  19. const kId = Symbol('id');
  20. /**
  21. * A class representation of the BSON ObjectId type.
  22. * @public
  23. * @category BSONType
  24. */
  25. export class ObjectId extends BSONValue {
  26. get _bsontype(): 'ObjectId' {
  27. return 'ObjectId';
  28. }
  29. /** @internal */
  30. private static index = Math.floor(Math.random() * 0xffffff);
  31. static cacheHexString: boolean;
  32. /** ObjectId Bytes @internal */
  33. private [kId]!: Uint8Array;
  34. /** ObjectId hexString cache @internal */
  35. private __id?: string;
  36. /**
  37. * Create an ObjectId type
  38. *
  39. * @param inputId - Can be a 24 character hex string, 12 byte binary Buffer, or a number.
  40. */
  41. constructor(inputId?: string | number | ObjectId | ObjectIdLike | Uint8Array) {
  42. super();
  43. // workingId is set based on type of input and whether valid id exists for the input
  44. let workingId;
  45. if (typeof inputId === 'object' && inputId && 'id' in inputId) {
  46. if (typeof inputId.id !== 'string' && !ArrayBuffer.isView(inputId.id)) {
  47. throw new BSONError('Argument passed in must have an id that is of type string or Buffer');
  48. }
  49. if ('toHexString' in inputId && typeof inputId.toHexString === 'function') {
  50. workingId = ByteUtils.fromHex(inputId.toHexString());
  51. } else {
  52. workingId = inputId.id;
  53. }
  54. } else {
  55. workingId = inputId;
  56. }
  57. // the following cases use workingId to construct an ObjectId
  58. if (workingId == null || typeof workingId === 'number') {
  59. // The most common use case (blank id, new objectId instance)
  60. // Generate a new id
  61. this[kId] = ObjectId.generate(typeof workingId === 'number' ? workingId : undefined);
  62. } else if (ArrayBuffer.isView(workingId) && workingId.byteLength === 12) {
  63. // If intstanceof matches we can escape calling ensure buffer in Node.js environments
  64. this[kId] = ByteUtils.toLocalBufferType(workingId);
  65. } else if (typeof workingId === 'string') {
  66. if (workingId.length === 12) {
  67. // TODO(NODE-4361): Remove string of length 12 support
  68. const bytes = ByteUtils.fromUTF8(workingId);
  69. if (bytes.byteLength === 12) {
  70. this[kId] = bytes;
  71. } else {
  72. throw new BSONError('Argument passed in must be a string of 12 bytes');
  73. }
  74. } else if (workingId.length === 24 && checkForHexRegExp.test(workingId)) {
  75. this[kId] = ByteUtils.fromHex(workingId);
  76. } else {
  77. throw new BSONError(
  78. 'Argument passed in must be a string of 12 bytes or a string of 24 hex characters or an integer'
  79. );
  80. }
  81. } else {
  82. throw new BSONError('Argument passed in does not match the accepted types');
  83. }
  84. // If we are caching the hex string
  85. if (ObjectId.cacheHexString) {
  86. this.__id = ByteUtils.toHex(this.id);
  87. }
  88. }
  89. /**
  90. * The ObjectId bytes
  91. * @readonly
  92. */
  93. get id(): Uint8Array {
  94. return this[kId];
  95. }
  96. set id(value: Uint8Array) {
  97. this[kId] = value;
  98. if (ObjectId.cacheHexString) {
  99. this.__id = ByteUtils.toHex(value);
  100. }
  101. }
  102. /** Returns the ObjectId id as a 24 character hex string representation */
  103. toHexString(): string {
  104. if (ObjectId.cacheHexString && this.__id) {
  105. return this.__id;
  106. }
  107. const hexString = ByteUtils.toHex(this.id);
  108. if (ObjectId.cacheHexString && !this.__id) {
  109. this.__id = hexString;
  110. }
  111. return hexString;
  112. }
  113. /**
  114. * Update the ObjectId index
  115. * @internal
  116. */
  117. private static getInc(): number {
  118. return (ObjectId.index = (ObjectId.index + 1) % 0xffffff);
  119. }
  120. /**
  121. * Generate a 12 byte id buffer used in ObjectId's
  122. *
  123. * @param time - pass in a second based timestamp.
  124. */
  125. static generate(time?: number): Uint8Array {
  126. if ('number' !== typeof time) {
  127. time = Math.floor(Date.now() / 1000);
  128. }
  129. const inc = ObjectId.getInc();
  130. const buffer = ByteUtils.allocate(12);
  131. // 4-byte timestamp
  132. BSONDataView.fromUint8Array(buffer).setUint32(0, time, false);
  133. // set PROCESS_UNIQUE if yet not initialized
  134. if (PROCESS_UNIQUE === null) {
  135. PROCESS_UNIQUE = ByteUtils.randomBytes(5);
  136. }
  137. // 5-byte process unique
  138. buffer[4] = PROCESS_UNIQUE[0];
  139. buffer[5] = PROCESS_UNIQUE[1];
  140. buffer[6] = PROCESS_UNIQUE[2];
  141. buffer[7] = PROCESS_UNIQUE[3];
  142. buffer[8] = PROCESS_UNIQUE[4];
  143. // 3-byte counter
  144. buffer[11] = inc & 0xff;
  145. buffer[10] = (inc >> 8) & 0xff;
  146. buffer[9] = (inc >> 16) & 0xff;
  147. return buffer;
  148. }
  149. /**
  150. * Converts the id into a 24 character hex string for printing, unless encoding is provided.
  151. * @param encoding - hex or base64
  152. */
  153. toString(encoding?: 'hex' | 'base64'): string {
  154. // Is the id a buffer then use the buffer toString method to return the format
  155. if (encoding === 'base64') return ByteUtils.toBase64(this.id);
  156. if (encoding === 'hex') return this.toHexString();
  157. return this.toHexString();
  158. }
  159. /** Converts to its JSON the 24 character hex string representation. */
  160. toJSON(): string {
  161. return this.toHexString();
  162. }
  163. /**
  164. * Compares the equality of this ObjectId with `otherID`.
  165. *
  166. * @param otherId - ObjectId instance to compare against.
  167. */
  168. equals(otherId: string | ObjectId | ObjectIdLike): boolean {
  169. if (otherId === undefined || otherId === null) {
  170. return false;
  171. }
  172. if (otherId instanceof ObjectId) {
  173. return this[kId][11] === otherId[kId][11] && ByteUtils.equals(this[kId], otherId[kId]);
  174. }
  175. if (
  176. typeof otherId === 'string' &&
  177. ObjectId.isValid(otherId) &&
  178. otherId.length === 12 &&
  179. isUint8Array(this.id)
  180. ) {
  181. return ByteUtils.equals(this.id, ByteUtils.fromISO88591(otherId));
  182. }
  183. if (typeof otherId === 'string' && ObjectId.isValid(otherId) && otherId.length === 24) {
  184. return otherId.toLowerCase() === this.toHexString();
  185. }
  186. if (typeof otherId === 'string' && ObjectId.isValid(otherId) && otherId.length === 12) {
  187. return ByteUtils.equals(ByteUtils.fromUTF8(otherId), this.id);
  188. }
  189. if (
  190. typeof otherId === 'object' &&
  191. 'toHexString' in otherId &&
  192. typeof otherId.toHexString === 'function'
  193. ) {
  194. const otherIdString = otherId.toHexString();
  195. const thisIdString = this.toHexString().toLowerCase();
  196. return typeof otherIdString === 'string' && otherIdString.toLowerCase() === thisIdString;
  197. }
  198. return false;
  199. }
  200. /** Returns the generation date (accurate up to the second) that this ID was generated. */
  201. getTimestamp(): Date {
  202. const timestamp = new Date();
  203. const time = BSONDataView.fromUint8Array(this.id).getUint32(0, false);
  204. timestamp.setTime(Math.floor(time) * 1000);
  205. return timestamp;
  206. }
  207. /** @internal */
  208. static createPk(): ObjectId {
  209. return new ObjectId();
  210. }
  211. /**
  212. * Creates an ObjectId from a second based number, with the rest of the ObjectId zeroed out. Used for comparisons or sorting the ObjectId.
  213. *
  214. * @param time - an integer number representing a number of seconds.
  215. */
  216. static createFromTime(time: number): ObjectId {
  217. const buffer = ByteUtils.fromNumberArray([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
  218. // Encode time into first 4 bytes
  219. BSONDataView.fromUint8Array(buffer).setUint32(0, time, false);
  220. // Return the new objectId
  221. return new ObjectId(buffer);
  222. }
  223. /**
  224. * Creates an ObjectId from a hex string representation of an ObjectId.
  225. *
  226. * @param hexString - create a ObjectId from a passed in 24 character hexstring.
  227. */
  228. static createFromHexString(hexString: string): ObjectId {
  229. if (hexString?.length !== 24) {
  230. throw new BSONError('hex string must be 24 characters');
  231. }
  232. return new ObjectId(ByteUtils.fromHex(hexString));
  233. }
  234. /** Creates an ObjectId instance from a base64 string */
  235. static createFromBase64(base64: string): ObjectId {
  236. if (base64?.length !== 16) {
  237. throw new BSONError('base64 string must be 16 characters');
  238. }
  239. return new ObjectId(ByteUtils.fromBase64(base64));
  240. }
  241. /**
  242. * Checks if a value is a valid bson ObjectId
  243. *
  244. * @param id - ObjectId instance to validate.
  245. */
  246. static isValid(id: string | number | ObjectId | ObjectIdLike | Uint8Array): boolean {
  247. if (id == null) return false;
  248. try {
  249. new ObjectId(id);
  250. return true;
  251. } catch {
  252. return false;
  253. }
  254. }
  255. /** @internal */
  256. toExtendedJSON(): ObjectIdExtended {
  257. if (this.toHexString) return { $oid: this.toHexString() };
  258. return { $oid: this.toString('hex') };
  259. }
  260. /** @internal */
  261. static fromExtendedJSON(doc: ObjectIdExtended): ObjectId {
  262. return new ObjectId(doc.$oid);
  263. }
  264. /**
  265. * Converts to a string representation of this Id.
  266. *
  267. * @returns return the 24 character hex string representation.
  268. * @internal
  269. */
  270. [Symbol.for('nodejs.util.inspect.custom')](): string {
  271. return this.inspect();
  272. }
  273. inspect(): string {
  274. return `new ObjectId("${this.toHexString()}")`;
  275. }
  276. }