|
|
import { BSONValue } from './bson_value'; |
|
|
import { BSONError } from './error'; |
|
|
import { isUint8Array } from './parser/utils'; |
|
|
import { BSONDataView, ByteUtils } from './utils/byte_utils'; |
|
|
|
|
|
|
|
|
const checkForHexRegExp = new RegExp('^[0-9a-fA-F]{24}$'); |
|
|
|
|
|
|
|
|
let PROCESS_UNIQUE: Uint8Array | null = null; |
|
|
|
|
|
|
|
|
export interface ObjectIdLike { |
|
|
id: string | Uint8Array; |
|
|
__id?: string; |
|
|
toHexString(): string; |
|
|
} |
|
|
|
|
|
|
|
|
export interface ObjectIdExtended { |
|
|
$oid: string; |
|
|
} |
|
|
|
|
|
const kId = Symbol('id'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export class ObjectId extends BSONValue { |
|
|
get _bsontype(): 'ObjectId' { |
|
|
return 'ObjectId'; |
|
|
} |
|
|
|
|
|
|
|
|
private static index = Math.floor(Math.random() * 0xffffff); |
|
|
|
|
|
static cacheHexString: boolean; |
|
|
|
|
|
|
|
|
private [kId]!: Uint8Array; |
|
|
|
|
|
private __id?: string; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(inputId?: string | number | ObjectId | ObjectIdLike | Uint8Array) { |
|
|
super(); |
|
|
|
|
|
let workingId; |
|
|
if (typeof inputId === 'object' && inputId && 'id' in inputId) { |
|
|
if (typeof inputId.id !== 'string' && !ArrayBuffer.isView(inputId.id)) { |
|
|
throw new BSONError('Argument passed in must have an id that is of type string or Buffer'); |
|
|
} |
|
|
if ('toHexString' in inputId && typeof inputId.toHexString === 'function') { |
|
|
workingId = ByteUtils.fromHex(inputId.toHexString()); |
|
|
} else { |
|
|
workingId = inputId.id; |
|
|
} |
|
|
} else { |
|
|
workingId = inputId; |
|
|
} |
|
|
|
|
|
|
|
|
if (workingId == null || typeof workingId === 'number') { |
|
|
|
|
|
|
|
|
this[kId] = ObjectId.generate(typeof workingId === 'number' ? workingId : undefined); |
|
|
} else if (ArrayBuffer.isView(workingId) && workingId.byteLength === 12) { |
|
|
|
|
|
this[kId] = ByteUtils.toLocalBufferType(workingId); |
|
|
} else if (typeof workingId === 'string') { |
|
|
if (workingId.length === 12) { |
|
|
|
|
|
const bytes = ByteUtils.fromUTF8(workingId); |
|
|
if (bytes.byteLength === 12) { |
|
|
this[kId] = bytes; |
|
|
} else { |
|
|
throw new BSONError('Argument passed in must be a string of 12 bytes'); |
|
|
} |
|
|
} else if (workingId.length === 24 && checkForHexRegExp.test(workingId)) { |
|
|
this[kId] = ByteUtils.fromHex(workingId); |
|
|
} else { |
|
|
throw new BSONError( |
|
|
'Argument passed in must be a string of 12 bytes or a string of 24 hex characters or an integer' |
|
|
); |
|
|
} |
|
|
} else { |
|
|
throw new BSONError('Argument passed in does not match the accepted types'); |
|
|
} |
|
|
|
|
|
if (ObjectId.cacheHexString) { |
|
|
this.__id = ByteUtils.toHex(this.id); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
get id(): Uint8Array { |
|
|
return this[kId]; |
|
|
} |
|
|
|
|
|
set id(value: Uint8Array) { |
|
|
this[kId] = value; |
|
|
if (ObjectId.cacheHexString) { |
|
|
this.__id = ByteUtils.toHex(value); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
toHexString(): string { |
|
|
if (ObjectId.cacheHexString && this.__id) { |
|
|
return this.__id; |
|
|
} |
|
|
|
|
|
const hexString = ByteUtils.toHex(this.id); |
|
|
|
|
|
if (ObjectId.cacheHexString && !this.__id) { |
|
|
this.__id = hexString; |
|
|
} |
|
|
|
|
|
return hexString; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static getInc(): number { |
|
|
return (ObjectId.index = (ObjectId.index + 1) % 0xffffff); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static generate(time?: number): Uint8Array { |
|
|
if ('number' !== typeof time) { |
|
|
time = Math.floor(Date.now() / 1000); |
|
|
} |
|
|
|
|
|
const inc = ObjectId.getInc(); |
|
|
const buffer = ByteUtils.allocate(12); |
|
|
|
|
|
|
|
|
BSONDataView.fromUint8Array(buffer).setUint32(0, time, false); |
|
|
|
|
|
|
|
|
if (PROCESS_UNIQUE === null) { |
|
|
PROCESS_UNIQUE = ByteUtils.randomBytes(5); |
|
|
} |
|
|
|
|
|
|
|
|
buffer[4] = PROCESS_UNIQUE[0]; |
|
|
buffer[5] = PROCESS_UNIQUE[1]; |
|
|
buffer[6] = PROCESS_UNIQUE[2]; |
|
|
buffer[7] = PROCESS_UNIQUE[3]; |
|
|
buffer[8] = PROCESS_UNIQUE[4]; |
|
|
|
|
|
|
|
|
buffer[11] = inc & 0xff; |
|
|
buffer[10] = (inc >> 8) & 0xff; |
|
|
buffer[9] = (inc >> 16) & 0xff; |
|
|
|
|
|
return buffer; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
toString(encoding?: 'hex' | 'base64'): string { |
|
|
|
|
|
if (encoding === 'base64') return ByteUtils.toBase64(this.id); |
|
|
if (encoding === 'hex') return this.toHexString(); |
|
|
return this.toHexString(); |
|
|
} |
|
|
|
|
|
|
|
|
toJSON(): string { |
|
|
return this.toHexString(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
equals(otherId: string | ObjectId | ObjectIdLike): boolean { |
|
|
if (otherId === undefined || otherId === null) { |
|
|
return false; |
|
|
} |
|
|
|
|
|
if (otherId instanceof ObjectId) { |
|
|
return this[kId][11] === otherId[kId][11] && ByteUtils.equals(this[kId], otherId[kId]); |
|
|
} |
|
|
|
|
|
if ( |
|
|
typeof otherId === 'string' && |
|
|
ObjectId.isValid(otherId) && |
|
|
otherId.length === 12 && |
|
|
isUint8Array(this.id) |
|
|
) { |
|
|
return ByteUtils.equals(this.id, ByteUtils.fromISO88591(otherId)); |
|
|
} |
|
|
|
|
|
if (typeof otherId === 'string' && ObjectId.isValid(otherId) && otherId.length === 24) { |
|
|
return otherId.toLowerCase() === this.toHexString(); |
|
|
} |
|
|
|
|
|
if (typeof otherId === 'string' && ObjectId.isValid(otherId) && otherId.length === 12) { |
|
|
return ByteUtils.equals(ByteUtils.fromUTF8(otherId), this.id); |
|
|
} |
|
|
|
|
|
if ( |
|
|
typeof otherId === 'object' && |
|
|
'toHexString' in otherId && |
|
|
typeof otherId.toHexString === 'function' |
|
|
) { |
|
|
const otherIdString = otherId.toHexString(); |
|
|
const thisIdString = this.toHexString().toLowerCase(); |
|
|
return typeof otherIdString === 'string' && otherIdString.toLowerCase() === thisIdString; |
|
|
} |
|
|
|
|
|
return false; |
|
|
} |
|
|
|
|
|
|
|
|
getTimestamp(): Date { |
|
|
const timestamp = new Date(); |
|
|
const time = BSONDataView.fromUint8Array(this.id).getUint32(0, false); |
|
|
timestamp.setTime(Math.floor(time) * 1000); |
|
|
return timestamp; |
|
|
} |
|
|
|
|
|
|
|
|
static createPk(): ObjectId { |
|
|
return new ObjectId(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static createFromTime(time: number): ObjectId { |
|
|
const buffer = ByteUtils.fromNumberArray([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); |
|
|
|
|
|
BSONDataView.fromUint8Array(buffer).setUint32(0, time, false); |
|
|
|
|
|
return new ObjectId(buffer); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static createFromHexString(hexString: string): ObjectId { |
|
|
if (hexString?.length !== 24) { |
|
|
throw new BSONError('hex string must be 24 characters'); |
|
|
} |
|
|
|
|
|
return new ObjectId(ByteUtils.fromHex(hexString)); |
|
|
} |
|
|
|
|
|
|
|
|
static createFromBase64(base64: string): ObjectId { |
|
|
if (base64?.length !== 16) { |
|
|
throw new BSONError('base64 string must be 16 characters'); |
|
|
} |
|
|
|
|
|
return new ObjectId(ByteUtils.fromBase64(base64)); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static isValid(id: string | number | ObjectId | ObjectIdLike | Uint8Array): boolean { |
|
|
if (id == null) return false; |
|
|
|
|
|
try { |
|
|
new ObjectId(id); |
|
|
return true; |
|
|
} catch { |
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
toExtendedJSON(): ObjectIdExtended { |
|
|
if (this.toHexString) return { $oid: this.toHexString() }; |
|
|
return { $oid: this.toString('hex') }; |
|
|
} |
|
|
|
|
|
|
|
|
static fromExtendedJSON(doc: ObjectIdExtended): ObjectId { |
|
|
return new ObjectId(doc.$oid); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[Symbol.for('nodejs.util.inspect.custom')](): string { |
|
|
return this.inspect(); |
|
|
} |
|
|
|
|
|
inspect(): string { |
|
|
return `new ObjectId("${this.toHexString()}")`; |
|
|
} |
|
|
} |
|
|
|