hadron-type-checker
Version:
Hadron Type Checker
566 lines (565 loc) • 18.8 kB
JavaScript
;
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();