UNPKG

hadron-type-checker

Version:
566 lines (565 loc) • 18.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.convertBinaryUUID = exports.reverseCSharpUUIDBytes = exports.reverseJavaUUIDBytes = exports.uuidHexToString = exports.UUID_REGEX = exports.UUID_TYPES = exports.bsonType = void 0; exports.getBsonType = getBsonType; exports.isUUIDType = isUUIDType; const lodash_1 = require("lodash"); const bson_1 = require("bson"); Object.defineProperty(exports, "bsonType", { enumerable: true, get: function () { return bson_1.bsonType; } }); function getBsonType(value) { return value?.[bson_1.bsonType]; } /** * The object string. */ const OBJECT = 'Object'; /** * The array type string. */ const ARRAY = 'Array'; /** * True constant. */ const TRUE = 'true'; /** * False constant. */ const FALSE = 'false'; /** * Long constant. */ const LONG = 'Long'; const INT_32 = 'Int32'; const INT_64 = 'Int64'; const DOUBLE = 'Double'; const DECIMAL_128 = 'Decimal128'; const OBJECT_TYPE = '[object Object]'; const EMPTY = ''; /** * The match regex. */ const MATCH = /\[object (\w+)\]/; /** * The max int 32 value. */ const BSON_INT32_MAX = 0x7fffffff; /** * The min int 32 value. */ const BSON_INT32_MIN = -0x80000000; const BSON_INT64_MAX = Math.pow(2, 63) - 1; const BSON_INT64_MIN = -BSON_INT64_MAX; /** * The number regex. */ const NUMBER_REGEX = /^-?\d+$/; /** * All bson types that are numbers. */ const NUMBER_TYPES = ['Long', 'Int32', 'Double', 'Decimal128']; const toDate = (object) => { return new Date(object); }; const toMinKey = () => { return new bson_1.MinKey(); }; const toMaxKey = () => { return new bson_1.MaxKey(); }; const toUndefined = () => { return undefined; }; const toNull = () => { return null; }; const toBoolean = (object) => { if ((0, lodash_1.isString)(object)) { if (object.toLowerCase() === TRUE) { return true; } else if (object.toLowerCase() === FALSE) { return false; } throw new Error(`'${object}' is not a valid boolean string`); } if (object) { return true; } return false; }; const toObject = (object) => { if ((0, lodash_1.isPlainObject)(object)) { return object; } return {}; }; const toArray = (object) => { if ((0, lodash_1.isArray)(object)) { return object; } if ((0, lodash_1.isPlainObject)(object)) { return []; } return [object]; }; const toInt32 = (object) => { if (object === '-' || object === '') { throw new Error(`Value '${object}' is not a valid Int32 value`); } const number = (0, lodash_1.toNumber)(object); if (number >= BSON_INT32_MIN && number <= BSON_INT32_MAX) { return new bson_1.Int32(number); } throw new Error(`Value ${number} is outside the valid Int32 range`); }; const toInt64 = (object) => { if (object === '-' || object === '') { throw new Error(`Value '${object}' is not a valid Int64 value`); } const number = (0, lodash_1.toNumber)(object); if (number >= BSON_INT64_MIN && number <= BSON_INT64_MAX) { // when casting from int32 object(this will have object.value) or literal // (it will a typeof number) we can safely create object fromNumber, as it // will not be greater than JS's max value if (object?.value || typeof object === 'number') { return bson_1.Long.fromNumber(number); } else if (typeof object === 'object' && object !== null && 'toString' in object) { // to make sure we are still displaying Very Large numbers properly, convert // the current 'object' to a string return bson_1.Long.fromString(object.toString()); } return bson_1.Long.fromString((0, lodash_1.toString)(object)); } throw new Error(`Value ${(0, lodash_1.toString)(object)} is outside the valid Int64 range`); }; const toDouble = (object) => { if (object === '-' || object === '') { throw new Error(`Value '${object}' is not a valid Double value`); } if ((0, lodash_1.isString)(object) && object.endsWith('.')) { throw new Error('Please enter at least one digit after the decimal'); } const number = (0, lodash_1.toNumber)(object); return new bson_1.Double(number); }; const toDecimal128 = (object) => { /* If converting a BSON Object, extract the value before converting to a string. */ if (NUMBER_TYPES.includes(getBsonType(object))) { const bsonObj = object; object = getBsonType(object) === LONG ? bsonObj.toString() : bsonObj.valueOf(); } return bson_1.Decimal128.fromString((0, lodash_1.toString)(object)); }; const toObjectID = (object) => { if (!(0, lodash_1.isString)(object) || object === '') { return new bson_1.ObjectId(); } return bson_1.ObjectId.createFromHexString(object); }; const toBinary = (object) => { const buffer = ArrayBuffer.isView(object) ? Buffer.from(object) : Buffer.from((0, lodash_1.toString)(object), 'utf8'); return new bson_1.Binary(buffer, bson_1.Binary.SUBTYPE_DEFAULT); }; exports.UUID_TYPES = [ 'UUID', 'LegacyJavaUUID', 'LegacyCSharpUUID', 'LegacyPythonUUID', ]; function isUUIDType(type) { return exports.UUID_TYPES.includes(type); } /** * UUID regex pattern for validation (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx). */ exports.UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; /** * Validates a UUID string format. */ const validateUUIDString = (uuidString) => { if (!exports.UUID_REGEX.test(uuidString)) { throw new Error(`'${uuidString}' is not a valid UUID string (expected format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)`); } }; /** * Converts a UUID string (with hyphens) to a hex string (without hyphens). */ const uuidStringToHex = (uuidString) => { return uuidString.replace(/-/g, ''); }; /** * Converts a hex string (without hyphens) to UUID format with hyphens. */ const uuidHexToString = (hex) => { return (hex.substring(0, 8) + '-' + hex.substring(8, 12) + '-' + hex.substring(12, 16) + '-' + hex.substring(16, 20) + '-' + hex.substring(20, 32)); }; exports.uuidHexToString = uuidHexToString; /** * Reverses byte order for Java legacy UUID format (both MSB and LSB). * Takes a 32-char hex string and returns a 32-char hex string with reversed byte order. * * This function is an involution (self-inverse), meaning applying it twice returns * the original value. It can be used both to: * - Convert from Java legacy binary format to standard UUID hex (for display) * - Convert from standard UUID hex to Java legacy binary format (for storage) */ const reverseJavaUUIDBytes = (hex) => { let msb = hex.substring(0, 16); let lsb = hex.substring(16, 32); msb = msb.substring(14, 16) + msb.substring(12, 14) + msb.substring(10, 12) + msb.substring(8, 10) + msb.substring(6, 8) + msb.substring(4, 6) + msb.substring(2, 4) + msb.substring(0, 2); lsb = lsb.substring(14, 16) + lsb.substring(12, 14) + lsb.substring(10, 12) + lsb.substring(8, 10) + lsb.substring(6, 8) + lsb.substring(4, 6) + lsb.substring(2, 4) + lsb.substring(0, 2); return msb + lsb; }; exports.reverseJavaUUIDBytes = reverseJavaUUIDBytes; /** * Reverses byte order for C# legacy UUID format (first 3 groups only). * Takes a 32-char hex string and returns a 32-char hex string with reversed byte order. * * This function is an involution (self-inverse), meaning applying it twice returns * the original value. It can be used both to: * - Convert from C# legacy binary format to standard UUID hex (for display) * - Convert from standard UUID hex to C# legacy binary format (for storage) */ const reverseCSharpUUIDBytes = (hex) => { const a = hex.substring(6, 8) + hex.substring(4, 6) + hex.substring(2, 4) + hex.substring(0, 2); const b = hex.substring(10, 12) + hex.substring(8, 10); const c = hex.substring(14, 16) + hex.substring(12, 14); const d = hex.substring(16, 32); return a + b + c + d; }; exports.reverseCSharpUUIDBytes = reverseCSharpUUIDBytes; /** * Generates a random UUID string in the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. */ const generateRandomUUID = () => { return new bson_1.UUID().toString(); }; /** * Converts a Binary UUID to a standard UUID string, accounting for its encoding. * For subtype 4 (standard UUID), returns the hex directly as UUID format. * For subtype 3 (legacy UUID), we need to know the original encoding to reverse the bytes. * If sourceEncoding is not provided, assumes Python encoding (no reversal). */ const binaryToUUIDStringWithEncoding = (binary, sourceEncoding) => { const hex = binary.toString('hex'); // For standard UUID (subtype 4), no byte reversal needed if (binary.sub_type === bson_1.Binary.SUBTYPE_UUID) { return (0, exports.uuidHexToString)(hex); } // For legacy UUID (subtype 3), reverse bytes based on source encoding switch (sourceEncoding) { case 'Java': // Reverse Java encoding to get standard UUID return (0, exports.uuidHexToString)((0, exports.reverseJavaUUIDBytes)(hex)); case 'CSharp': // Reverse C# encoding to get standard UUID return (0, exports.uuidHexToString)((0, exports.reverseCSharpUUIDBytes)(hex)); case 'Python': default: // Python uses standard byte order, no reversal needed return (0, exports.uuidHexToString)(hex); } }; /** * Gets the UUID string from an object, handling Binary inputs specially. * For Binary inputs, extracts and converts to UUID string format. * For string inputs, returns as-is after trimming. */ const getUUIDStringFromObject = (object, sourceEncoding) => { if (object instanceof bson_1.Binary) { if (object.sub_type === bson_1.Binary.SUBTYPE_UUID || object.sub_type === bson_1.Binary.SUBTYPE_UUID_OLD) { return binaryToUUIDStringWithEncoding(object, sourceEncoding); } } return (0, lodash_1.toString)(object).trim(); }; /** * Mapping from UUID type names to encoding names. */ const UUID_TYPE_TO_ENCODING = Object.assign(Object.create(null), { UUID: undefined, LegacyJavaUUID: 'Java', LegacyCSharpUUID: 'CSharp', LegacyPythonUUID: 'Python', }); /** * Converts a Binary UUID from one encoding to another. * This is used when changing between UUID types in the document editor. * * @param binary - The source Binary UUID * @param sourceType - The source UUID type (e.g., 'LegacyCSharpUUID') * @param targetType - The target UUID type (e.g., 'LegacyJavaUUID') * @returns A new Binary with the same UUID value but different encoding */ const convertBinaryUUID = (binary, sourceType, targetType) => { // Get the source encoding to decode the binary const sourceEncoding = UUID_TYPE_TO_ENCODING[sourceType]; // Convert binary to standard UUID string using source encoding const uuidString = binaryToUUIDStringWithEncoding(binary, sourceEncoding); // Convert UUID string to binary using target encoding const hex = uuidStringToHex(uuidString); switch (targetType) { case 'UUID': return bson_1.Binary.createFromHexString(hex, bson_1.Binary.SUBTYPE_UUID); case 'LegacyJavaUUID': return bson_1.Binary.createFromHexString((0, exports.reverseJavaUUIDBytes)(hex), bson_1.Binary.SUBTYPE_UUID_OLD); case 'LegacyCSharpUUID': return bson_1.Binary.createFromHexString((0, exports.reverseCSharpUUIDBytes)(hex), bson_1.Binary.SUBTYPE_UUID_OLD); case 'LegacyPythonUUID': return bson_1.Binary.createFromHexString(hex, bson_1.Binary.SUBTYPE_UUID_OLD); default: throw new Error(`Unknown UUID type: ${targetType}`); } }; exports.convertBinaryUUID = convertBinaryUUID; /** * Converts to UUID (Binary subtype 4). * If the input is empty, generates a random UUID. * If the input is a Binary, extracts the UUID from it. */ const toUUID = (object) => { const uuidString = getUUIDStringFromObject(object); if (!uuidString) { return new bson_1.UUID().toBinary(); } validateUUIDString(uuidString); const hex = uuidStringToHex(uuidString); return bson_1.Binary.createFromHexString(hex, bson_1.Binary.SUBTYPE_UUID); }; /** * Converts to Legacy Java UUID (Binary subtype 3). * Java legacy format reverses byte order for both MSB and LSB. * If the input is empty, generates a random UUID. * If the input is a Binary, extracts the UUID from it. */ const toLegacyJavaUUID = (object) => { let uuidString = getUUIDStringFromObject(object, 'Java'); if (!uuidString) { uuidString = generateRandomUUID(); } else { validateUUIDString(uuidString); } const hex = uuidStringToHex(uuidString); const reversedHex = (0, exports.reverseJavaUUIDBytes)(hex); return bson_1.Binary.createFromHexString(reversedHex, bson_1.Binary.SUBTYPE_UUID_OLD); }; /** * Converts to Legacy C# UUID (Binary subtype 3). * C# legacy format reverses byte order for first 3 groups only. * If the input is empty, generates a random UUID. * If the input is a Binary, extracts the UUID from it. */ const toLegacyCSharpUUID = (object) => { let uuidString = getUUIDStringFromObject(object, 'CSharp'); if (!uuidString) { uuidString = generateRandomUUID(); } else { validateUUIDString(uuidString); } const hex = uuidStringToHex(uuidString); const reversedHex = (0, exports.reverseCSharpUUIDBytes)(hex); return bson_1.Binary.createFromHexString(reversedHex, bson_1.Binary.SUBTYPE_UUID_OLD); }; /** * Converts to Legacy Python UUID (Binary subtype 3). * Python legacy format uses direct byte order (no reversal). * If the input is empty, generates a random UUID. * If the input is a Binary, extracts the UUID from it. */ const toLegacyPythonUUID = (object) => { let uuidString = getUUIDStringFromObject(object, 'Python'); if (!uuidString) { uuidString = generateRandomUUID(); } else { validateUUIDString(uuidString); } const hex = uuidStringToHex(uuidString); return bson_1.Binary.createFromHexString(hex, bson_1.Binary.SUBTYPE_UUID_OLD); }; const toRegex = (object) => { return new bson_1.BSONRegExp((0, lodash_1.toString)(object)); }; const toCode = (object) => { return new bson_1.Code((0, lodash_1.toString)(object)); }; const toSymbol = (object) => { return new bson_1.BSONSymbol((0, lodash_1.toString)(object)); }; const toTimestamp = (object) => { const number = (0, lodash_1.toNumber)(object); return bson_1.Timestamp.fromNumber(number); }; /** * The functions to cast to a type. */ const CASTERS = { Array: toArray, Binary: toBinary, Boolean: toBoolean, Code: toCode, Date: toDate, Decimal128: toDecimal128, Double: toDouble, Int32: toInt32, Int64: toInt64, MaxKey: toMaxKey, MinKey: toMinKey, Null: toNull, Object: toObject, ObjectId: toObjectID, BSONRegExp: toRegex, String: lodash_1.toString, BSONSymbol: toSymbol, Timestamp: toTimestamp, Undefined: toUndefined, UUID: toUUID, LegacyJavaUUID: toLegacyJavaUUID, LegacyCSharpUUID: toLegacyCSharpUUID, LegacyPythonUUID: toLegacyPythonUUID, }; /** * An array of all bson types. */ const TYPES = (0, lodash_1.keys)(CASTERS); /** * Checks if a string is an int32. */ class Int32Check { test(string) { if (NUMBER_REGEX.test(string)) { const value = (0, lodash_1.toNumber)(string); return value >= BSON_INT32_MIN && value <= BSON_INT32_MAX; } return false; } } /** * Checks if a string is an int64. */ class Int64Check { test(string) { if (NUMBER_REGEX.test(string)) { return Number.isSafeInteger((0, lodash_1.toNumber)(string)); } return false; } } const INT32_CHECK = new Int32Check(); const INT64_CHECK = new Int64Check(); /** * Gets the BSON type for a JS number. */ const numberToBsonType = (number) => { const string = (0, lodash_1.toString)(number); if (INT32_CHECK.test(string)) { return INT_32; } else if (INT64_CHECK.test(string)) { return INT_64; } return DOUBLE; }; /** * Checks the types of objects and returns them as readable strings. */ class TypeChecker { /** * Cast the provided object to the desired type. */ cast(object, type) { const caster = CASTERS[type]; let result = object; if (caster) { result = caster(object); } return (result === OBJECT_TYPE && result !== object ? EMPTY : result); } /** * Get the type for the object. * @param legacyUUIDEncoding - Optional encoding for legacy UUID (subtype 3). * If provided and the object is a Binary with subtype 3, returns the specific legacy UUID type. * Valid values: 'LegacyJavaUUID', 'LegacyCSharpUUID', 'LegacyPythonUUID' */ type(object, legacyUUIDEncoding) { const bsonType = getBsonType(object); if (bsonType) { if (bsonType === LONG) { return INT_64; } // Handle Binary UUID subtypes if (bsonType === 'Binary') { const binary = object; if (binary.sub_type === bson_1.Binary.SUBTYPE_UUID) { return 'UUID'; } if (binary.sub_type === bson_1.Binary.SUBTYPE_UUID_OLD && binary.buffer.length === 16 && legacyUUIDEncoding) { return legacyUUIDEncoding; } } return bsonType; } if ((0, lodash_1.isNumber)(object)) { return numberToBsonType(object); } if ((0, lodash_1.isPlainObject)(object)) { return OBJECT; } if ((0, lodash_1.isArray)(object)) { return ARRAY; } return Object.prototype.toString .call(object) .replace(MATCH, '$1'); } /** * Get a list of types the object can be cast to. */ castableTypes(highPrecisionSupport = false) { if (highPrecisionSupport === true) { return TYPES; } return (0, lodash_1.without)(TYPES, DECIMAL_128); } } exports.default = new TypeChecker();