From 65cef72ef115cf2b77d698fec0cb4ae1d5e969c7 Mon Sep 17 00:00:00 2001 From: Ivan Medina Date: Wed, 4 Feb 2026 10:38:29 -0300 Subject: [PATCH 01/15] feat(crud): add UUID type editing support in document editor COMPASS-10194 Add comprehensive support for UUID types (UUID, LegacyJavaUUID, LegacyCSharpUUID, LegacyPythonUUID) in the document editing interface. Changes include: - Add UUID types to type dropdown menu in document editor - Implement UUIDEditor for editing UUID values as strings - Add byte-order conversion for legacy UUID formats - Display UUID (subtype 4) as $uuid format in JSON view - Standardize on single quotes for UUID display consistency - Pass legacyUUIDDisplayEncoding preference to table view --- .../src/components/bson-value.spec.tsx | 6 +- .../src/components/bson-value.tsx | 112 +++++++++++- .../document-list/element-editors.tsx | 62 ++++++- .../src/components/document-list/element.tsx | 48 ++++- .../src/components/document-list/index.ts | 4 + .../src/components/document-list.tsx | 10 +- .../components/table-view/cell-renderer.tsx | 27 +-- .../table-view/document-table-view.tsx | 3 + packages/hadron-document/src/editor/index.ts | 7 +- packages/hadron-document/src/editor/uuid.ts | 170 ++++++++++++++++++ packages/hadron-document/src/element.ts | 25 ++- packages/hadron-document/src/utils.ts | 32 ++++ .../hadron-document/test/editor/uuid.spec.ts | 164 +++++++++++++++++ packages/hadron-type-checker/src/index.ts | 2 +- .../hadron-type-checker/src/type-checker.ts | 166 ++++++++++++++++- .../test/type-checker.test.ts | 8 + 16 files changed, 823 insertions(+), 23 deletions(-) create mode 100644 packages/hadron-document/src/editor/uuid.ts create mode 100644 packages/hadron-document/test/editor/uuid.spec.ts diff --git a/packages/compass-components/src/components/bson-value.spec.tsx b/packages/compass-components/src/components/bson-value.spec.tsx index 06bf64564bf..4c6dc11f500 100644 --- a/packages/compass-components/src/components/bson-value.spec.tsx +++ b/packages/compass-components/src/components/bson-value.spec.tsx @@ -192,7 +192,7 @@ describe('BSONValue', function () { ); expect(container.querySelector('.element-value')?.textContent).to.eq( - 'LegacyJavaUUID("efcdab89-6745-2301-efcd-ab8967452301")' + "LegacyJavaUUID('efcdab89-6745-2301-efcd-ab8967452301')" ); }); @@ -204,7 +204,7 @@ describe('BSONValue', function () { ); expect(container.querySelector('.element-value')?.textContent).to.eq( - 'LegacyCSharpUUID("67452301-ab89-efcd-0123-456789abcdef")' + "LegacyCSharpUUID('67452301-ab89-efcd-0123-456789abcdef')" ); }); @@ -216,7 +216,7 @@ describe('BSONValue', function () { ); expect(container.querySelector('.element-value')?.textContent).to.eq( - 'LegacyPythonUUID("01234567-89ab-cdef-0123-456789abcdef")' + "LegacyPythonUUID('01234567-89ab-cdef-0123-456789abcdef')" ); }); diff --git a/packages/compass-components/src/components/bson-value.tsx b/packages/compass-components/src/components/bson-value.tsx index 51f82ce8b28..51cb3291c53 100644 --- a/packages/compass-components/src/components/bson-value.tsx +++ b/packages/compass-components/src/components/bson-value.tsx @@ -165,7 +165,7 @@ const toLegacyJavaUUID = ({ value }: PropsByValueType<'Binary'>) => { lsb.substring(2, 4) + lsb.substring(0, 2); const uuid = msb + lsb; - return 'LegacyJavaUUID("' + toUUIDWithHyphens(uuid) + '")'; + return "LegacyJavaUUID('" + toUUIDWithHyphens(uuid) + "')"; }; const toLegacyCSharpUUID = ({ value }: PropsByValueType<'Binary'>) => { @@ -181,13 +181,13 @@ const toLegacyCSharpUUID = ({ value }: PropsByValueType<'Binary'>) => { const c = hex.substring(14, 16) + hex.substring(12, 14); const d = hex.substring(16, 32); const uuid = a + b + c + d; - return 'LegacyCSharpUUID("' + toUUIDWithHyphens(uuid) + '")'; + return "LegacyCSharpUUID('" + toUUIDWithHyphens(uuid) + "')"; }; const toLegacyPythonUUID = ({ value }: PropsByValueType<'Binary'>) => { // Get the hex representation from the buffer. const hex = Buffer.from(value.buffer).toString('hex'); - return 'LegacyPythonUUID("' + toUUIDWithHyphens(hex) + '")'; + return "LegacyPythonUUID('" + toUUIDWithHyphens(hex) + "')"; }; // Binary sub_type 3. @@ -227,6 +227,100 @@ const LegacyUUIDValue: React.FunctionComponent> = ( ); }; +// UUID value component for UUID type (Binary subtype 4) +const UUIDValue: React.FunctionComponent> = ({ + value, +}) => { + const stringifiedValue = useMemo(() => { + // During editing, value might be a string + if (typeof value === 'string') { + return value; + } + if (!value || !value.buffer) { + return String(value); + } + try { + return value.toUUID().toString(); + } catch { + return Buffer.from(value.buffer).toString('hex'); + } + }, [value]); + + return ( + + UUID(' + {stringifiedValue} + ') + + ); +}; + +// Legacy Java UUID value component +const LegacyJavaUUIDValue: React.FunctionComponent< + PropsByValueType<'LegacyJavaUUID'> +> = ({ value }) => { + const stringifiedValue = useMemo(() => { + // During editing, value might be a string + if (typeof value === 'string') { + return `LegacyJavaUUID('${value}')`; + } + if (!value || !value.buffer) { + return String(value); + } + return toLegacyJavaUUID({ value }); + }, [value]); + + return ( + + {stringifiedValue} + + ); +}; + +// Legacy C# UUID value component +const LegacyCSharpUUIDValue: React.FunctionComponent< + PropsByValueType<'LegacyCSharpUUID'> +> = ({ value }) => { + const stringifiedValue = useMemo(() => { + // During editing, value might be a string + if (typeof value === 'string') { + return `LegacyCSharpUUID('${value}')`; + } + if (!value || !value.buffer) { + return String(value); + } + return toLegacyCSharpUUID({ value }); + }, [value]); + + return ( + + {stringifiedValue} + + ); +}; + +// Legacy Python UUID value component +const LegacyPythonUUIDValue: React.FunctionComponent< + PropsByValueType<'LegacyPythonUUID'> +> = ({ value }) => { + const stringifiedValue = useMemo(() => { + // During editing, value might be a string + if (typeof value === 'string') { + return `LegacyPythonUUID('${value}')`; + } + if (!value || !value.buffer) { + return String(value); + } + return toLegacyPythonUUID({ value }); + }, [value]); + + return ( + + {stringifiedValue} + + ); +}; + const BinaryValue: React.FunctionComponent> = ({ value, }) => { @@ -486,6 +580,18 @@ const BSONValue: React.FunctionComponent = (props) => { return ; } return ; + case 'UUID': + return ; + case 'LegacyJavaUUID': + return ; + case 'LegacyCSharpUUID': + return ( + + ); + case 'LegacyPythonUUID': + return ( + + ); case 'Int32': case 'Double': return ; diff --git a/packages/compass-components/src/components/document-list/element-editors.tsx b/packages/compass-components/src/components/document-list/element-editors.tsx index 7deab9b541b..45a896db79b 100644 --- a/packages/compass-components/src/components/document-list/element-editors.tsx +++ b/packages/compass-components/src/components/document-list/element-editors.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from 'react'; import type { Element as HadronElementType } from 'hadron-document'; import type { TypeCastMap } from 'hadron-type-checker'; -import TypeChecker from 'hadron-type-checker'; +import TypeChecker, { UUID_REGEX } from 'hadron-type-checker'; import { css, cx } from '@leafygreen-ui/emotion'; import { palette } from '@leafygreen-ui/palette'; import { spacing } from '@leafygreen-ui/tokens'; @@ -11,6 +11,13 @@ import { documentTypography } from './typography'; import { Icon, Tooltip } from '../leafygreen'; import { useDarkMode } from '../../hooks/use-theme'; +const UUID_TYPES = [ + 'UUID', + 'LegacyJavaUUID', + 'LegacyCSharpUUID', + 'LegacyPythonUUID', +] as const; + const maxWidth = css({ maxWidth: '100%', overflowX: 'hidden', @@ -162,6 +169,29 @@ const editorTextarea = css({ color: 'inherit', }); +// UUID editor container that shows: UUID(" ") +const uuidEditorContainer = css({ + display: 'inline-flex', + alignItems: 'center', + maxWidth: '100%', +}); + +const uuidEditorInput = css({ + display: 'inline-block', + whiteSpace: 'nowrap', + verticalAlign: 'top', + color: 'inherit', +}); + +const uuidEditorLabel = css({ + userSelect: 'none', + whiteSpace: 'nowrap', +}); + +function isUUIDType(type: string): boolean { + return (UUID_TYPES as readonly string[]).includes(type); +} + export const ValueEditor: React.FunctionComponent<{ editing?: boolean; onEditStart(): void; @@ -268,6 +298,36 @@ export const ValueEditor: React.FunctionComponent<{ {...(mergedProps as React.HTMLProps)} > + ) : isUUIDType(type) ? ( +
+ {type}(' + { + onChange(evt.currentTarget.value); + }} + // See ./element.tsx + // eslint-disable-next-line jsx-a11y/no-autofocus + autoFocus={autoFocus} + className={cx( + editorReset, + editorOutline, + uuidEditorInput, + !valid && editorInvalid, + !valid && + (darkMode + ? editorInvalidDarkMode + : editorInvalidLightMode) + )} + spellCheck="false" + style={{ width: `${Math.max(val.length, 36)}ch` }} + {...(mergedProps as React.HTMLProps)} + > + ') +
) : ( = (props) => { readOnly: preferencesReadOnly, readWrite: preferencesReadWrite, enableImportExport: isImportExportEnabled, - } = usePreferences(['readOnly', 'readWrite', 'enableImportExport']); + legacyUUIDDisplayEncoding, + } = usePreferences([ + 'readOnly', + 'readWrite', + 'enableImportExport', + 'legacyUUIDDisplayEncoding', + ]); const isEditable = !preferencesReadOnly && @@ -508,6 +514,7 @@ const DocumentList: React.FunctionComponent = (props) => { scrollTriggerRef={scrollTriggerRef} columnWidths={columnWidths} onColumnWidthChange={onColumnWidthChange} + legacyUUIDDisplayEncoding={legacyUUIDDisplayEncoding} /> ); } @@ -530,6 +537,7 @@ const DocumentList: React.FunctionComponent = (props) => { currentViewInitialScrollTop, columnWidths, onColumnWidthChange, + legacyUUIDDisplayEncoding, ] ); diff --git a/packages/compass-crud/src/components/table-view/cell-renderer.tsx b/packages/compass-crud/src/components/table-view/cell-renderer.tsx index 963c9fd127c..af474aba242 100644 --- a/packages/compass-crud/src/components/table-view/cell-renderer.tsx +++ b/packages/compass-crud/src/components/table-view/cell-renderer.tsx @@ -2,12 +2,14 @@ import React from 'react'; import { BSONValue, css, + DocumentList, Icon, IconButton, LeafyGreenProvider, spacing, withDarkMode, } from '@mongodb-js/compass-components'; +import type { DocumentList as DocumentListTypes } from '@mongodb-js/compass-components'; import { Element } from 'hadron-document'; import type { ICellRendererReactComp } from 'ag-grid-react'; import type { ICellRendererParams } from 'ag-grid-community'; @@ -89,6 +91,7 @@ export type CellRendererProps = Omit & { drillDown: CrudActions['drillDown']; tz: string; darkMode?: boolean; + legacyUUIDDisplayEncoding?: DocumentListTypes.LegacyUUIDDisplay; }; /** @@ -347,18 +350,22 @@ class CellRenderer return ( // `ag-grid` renders this component outside of the context chain - // so we re-supply the dark mode theme here. + // so we re-supply the dark mode theme and legacy UUID encoding here. - {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/interactive-supports-focus*/} -
- {this.renderUndo(canUndo, canExpand)} - {this.renderExpand(canExpand)} - {element} -
+ {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/interactive-supports-focus*/} +
+ {this.renderUndo(canUndo, canExpand)} + {this.renderExpand(canExpand)} + {element} +
+
); } diff --git a/packages/compass-crud/src/components/table-view/document-table-view.tsx b/packages/compass-crud/src/components/table-view/document-table-view.tsx index 3d2e27de0d0..fb01cc8e11e 100644 --- a/packages/compass-crud/src/components/table-view/document-table-view.tsx +++ b/packages/compass-crud/src/components/table-view/document-table-view.tsx @@ -71,6 +71,7 @@ export type DocumentTableViewProps = { tz: string; className?: string; darkMode?: boolean; + legacyUUIDDisplayEncoding?: string; columnWidths: Record; onColumnWidthChange: (newColumnWidths: Record) => void; }; @@ -750,6 +751,7 @@ export class DocumentTableView extends React.Component { parentType: '', tz: this.props.tz, darkMode: this.props.darkMode, + legacyUUIDDisplayEncoding: this.props.legacyUUIDDisplayEncoding, }, editable: false, cellEditorFramework: CellEditor, @@ -823,6 +825,7 @@ export class DocumentTableView extends React.Component { parentType: parentType, tz: this.props.tz, darkMode: this.props.darkMode, + legacyUUIDDisplayEncoding: this.props.legacyUUIDDisplayEncoding, }, editable: function (params) { diff --git a/packages/hadron-document/src/editor/index.ts b/packages/hadron-document/src/editor/index.ts index 673c971aa11..26870b792c0 100644 --- a/packages/hadron-document/src/editor/index.ts +++ b/packages/hadron-document/src/editor/index.ts @@ -8,6 +8,7 @@ import DateEditor from './date'; import NullEditor from './null'; import UndefinedEditor from './undefined'; import ObjectIdEditor from './objectid'; +import UUIDEditor from './uuid'; import type { Element } from '../element'; const init = (element: Element) => ({ @@ -21,6 +22,7 @@ const init = (element: Element) => ({ Null: new NullEditor(element), Undefined: new UndefinedEditor(element), ObjectId: new ObjectIdEditor(element), + UUID: new UUIDEditor(element), }); export const ElementEditor = Object.assign(init, { @@ -34,6 +36,7 @@ export const ElementEditor = Object.assign(init, { NullEditor, UndefinedEditor, ObjectIdEditor, + UUIDEditor, }); export type Editor = @@ -46,7 +49,8 @@ export type Editor = | Int64Editor | NullEditor | UndefinedEditor - | ObjectIdEditor; + | ObjectIdEditor + | UUIDEditor; export { DateEditor, @@ -59,4 +63,5 @@ export { NullEditor, UndefinedEditor, ObjectIdEditor, + UUIDEditor, }; diff --git a/packages/hadron-document/src/editor/uuid.ts b/packages/hadron-document/src/editor/uuid.ts new file mode 100644 index 00000000000..5efd4580245 --- /dev/null +++ b/packages/hadron-document/src/editor/uuid.ts @@ -0,0 +1,170 @@ +import TypeChecker from 'hadron-type-checker'; +import { Binary } from 'bson'; +import { ElementEvents } from '../element-events'; +import StandardEditor from './standard'; +import type { Element } from '../element'; +import { UUID_TYPES, type UUIDType } from '../element'; +import type { BSONValue } from '../utils'; + +/** + * Converts a hex string to UUID format with hyphens. + */ +const toUUIDWithHyphens = (hex: string): string => { + return ( + hex.substring(0, 8) + + '-' + + hex.substring(8, 12) + + '-' + + hex.substring(12, 16) + + '-' + + hex.substring(16, 20) + + '-' + + hex.substring(20, 32) + ); +}; + +/** + * Converts a Binary UUID (subtype 4) to a UUID string with hyphens. + */ +const binaryToUUIDString = (binary: Binary): string => { + try { + return binary.toUUID().toString(); + } catch { + // Fallback to hex if toUUID fails + return Buffer.from(binary.buffer).toString('hex'); + } +}; + +/** + * Converts a Binary Legacy Java UUID (subtype 3) to a UUID string with hyphens. + * Java legacy format reverses byte order for both MSB and LSB. + */ +const binaryToLegacyJavaUUIDString = (binary: Binary): string => { + const hex = Buffer.from(binary.buffer).toString('hex'); + let msb = hex.substring(0, 16); + let lsb = hex.substring(16, 32); + // Reverse pairs of hex characters (bytes) for both MSB and LSB. + 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 toUUIDWithHyphens(msb + lsb); +}; + +/** + * Converts a Binary Legacy C# UUID (subtype 3) to a UUID string with hyphens. + * C# legacy format reverses byte order for first 3 groups only. + */ +const binaryToLegacyCSharpUUIDString = (binary: Binary): string => { + const hex = Buffer.from(binary.buffer).toString('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 toUUIDWithHyphens(a + b + c + d); +}; + +/** + * Converts a Binary Legacy Python UUID (subtype 3) to a UUID string with hyphens. + * Python legacy format uses direct byte order (no reversal). + */ +const binaryToLegacyPythonUUIDString = (binary: Binary): string => { + const hex = Buffer.from(binary.buffer).toString('hex'); + return toUUIDWithHyphens(hex); +}; + +/** + * CRUD editor for UUID values (Binary subtypes 3 and 4). + */ +export default class UUIDEditor extends StandardEditor { + uuidType: UUIDType; + + constructor(element: Element) { + super(element); + this.uuidType = (UUID_TYPES as readonly string[]).includes( + element.currentType + ) + ? (element.currentType as UUIDType) + : 'UUID'; + } + + /** + * Get the value being edited as a UUID string. + */ + value(): string { + const val = this.element.currentValue; + // If already a string (during editing), return as is + if (typeof val === 'string') { + return val; + } + // If it's a Binary, convert to UUID string based on the type + if (val instanceof Binary) { + switch (this.uuidType) { + case 'LegacyJavaUUID': + return binaryToLegacyJavaUUIDString(val); + case 'LegacyCSharpUUID': + return binaryToLegacyCSharpUUIDString(val); + case 'LegacyPythonUUID': + return binaryToLegacyPythonUUIDString(val); + case 'UUID': + default: + return binaryToUUIDString(val); + } + } + return String(val); + } + + /** + * Edit the element with the provided value. + */ + edit(value: BSONValue): void { + try { + TypeChecker.cast(value, this.uuidType); + this.element.currentValue = value; + this.element.setValid(); + this.element._bubbleUp(ElementEvents.Edited, this.element); + } catch (e: any) { + this.element.setInvalid(value, this.element.currentType, e.message); + } + } + + /** + * Start the UUID edit - convert Binary to string for editing. + */ + start(): void { + super.start(); + if (this.element.isCurrentTypeValid()) { + this.edit(this.value()); + } + } + + /** + * Complete the UUID edit by converting the valid string back to Binary. + */ + complete(): void { + super.complete(); + if (this.element.isCurrentTypeValid()) { + this.element.edit( + TypeChecker.cast(this.element.currentValue, this.uuidType) + ); + } + } +} diff --git a/packages/hadron-document/src/element.ts b/packages/hadron-document/src/element.ts index 0847a7e78cf..8a7ece467ef 100644 --- a/packages/hadron-document/src/element.ts +++ b/packages/hadron-document/src/element.ts @@ -52,6 +52,18 @@ const UNEDITABLE_TYPES = [ 'DBRef', ]; +/** + * UUID type names for Binary subtypes 3 and 4. + */ +export const UUID_TYPES = [ + 'UUID', + 'LegacyJavaUUID', + 'LegacyCSharpUUID', + 'LegacyPythonUUID', +] as const; + +export type UUIDType = (typeof UUID_TYPES)[number]; + export const DEFAULT_VISIBLE_ELEMENTS = 25; export function isValueExpandable( value: BSONValue @@ -247,6 +259,13 @@ export class Element extends EventEmitter { editor.complete(); } else { this.edit(TypeChecker.cast(this.generateObject(), newType)); + // For UUID types, explicitly set the currentType since TypeChecker.type() + // may not return the specific UUID type for legacy UUIDs (subtype 3) + if ((UUID_TYPES as readonly string[]).includes(newType)) { + this.currentType = newType; + // Fire another event to notify the UI of the type change + this._bubbleUp(ElementEvents.Edited, this); + } } } catch (e: unknown) { this.setInvalid(this.currentValue, newType, (e as Error).message); @@ -651,9 +670,13 @@ export class Element extends EventEmitter { * @returns If the value is editable. */ isValueEditable(): boolean { + // UUID types are editable even though Binary is in UNEDITABLE_TYPES + const isUUIDType = (UUID_TYPES as readonly string[]).includes( + this.currentType + ); return ( this._isKeyLegallyEditable() && - !UNEDITABLE_TYPES.includes(this.currentType) + (isUUIDType || !UNEDITABLE_TYPES.includes(this.currentType)) ); } diff --git a/packages/hadron-document/src/utils.ts b/packages/hadron-document/src/utils.ts index e552cb639df..a807632e8c5 100644 --- a/packages/hadron-document/src/utils.ts +++ b/packages/hadron-document/src/utils.ts @@ -77,6 +77,26 @@ export function objectToIdiomaticEJSON( ); } +/** + * Convert a base64 string to a UUID string (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx). + */ +function base64ToUUIDString(base64: string): string { + // Decode base64 to bytes + const bytes = Buffer.from(base64, 'base64'); + if (bytes.length !== 16) { + return ''; // Invalid UUID length + } + const hex = bytes.toString('hex'); + // Format as UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + return [ + hex.substring(0, 8), + hex.substring(8, 12), + hex.substring(12, 16), + hex.substring(16, 20), + hex.substring(20, 32), + ].join('-'); +} + function makeEJSONIdiomatic(value: any): void { if (!value || typeof value !== 'object') return; @@ -107,6 +127,18 @@ function makeEJSONIdiomatic(value: any): void { entry.$date = new Date(+number).toISOString(); } } + // Convert Binary subtype 04 (UUID) to $uuid format for better readability + if ( + entry.$binary && + entry.$binary.subType === '04' && + entry.$binary.base64 + ) { + const uuidString = base64ToUUIDString(entry.$binary.base64); + if (uuidString) { + value[key] = { $uuid: uuidString }; + continue; + } + } makeEJSONIdiomatic(entry); } } diff --git a/packages/hadron-document/test/editor/uuid.spec.ts b/packages/hadron-document/test/editor/uuid.spec.ts new file mode 100644 index 00000000000..70eedd8781a --- /dev/null +++ b/packages/hadron-document/test/editor/uuid.spec.ts @@ -0,0 +1,164 @@ +import { Binary } from 'bson'; +import { Element } from '../../src'; +import { UUIDEditor } from '../../src/editor'; +import { expect } from 'chai'; + +describe('UUIDEditor', function () { + const uuidString = '01234567-89ab-cdef-0123-456789abcdef'; + const uuidHex = '0123456789abcdef0123456789abcdef'; + + describe('#value', function () { + context('when the element is a UUID (subtype 4)', function () { + const binary = Binary.createFromHexString(uuidHex, Binary.SUBTYPE_UUID); + const element = new Element('uuid', binary, false); + element.currentType = 'UUID'; + const uuidEditor = new UUIDEditor(element); + + it('returns the UUID string with hyphens', function () { + expect(uuidEditor.value()).to.equal(uuidString); + }); + }); + + context('when the element is a LegacyPythonUUID (subtype 3)', function () { + const binary = Binary.createFromHexString( + uuidHex, + Binary.SUBTYPE_UUID_OLD + ); + const element = new Element('uuid', binary, false); + element.currentType = 'LegacyPythonUUID'; + const uuidEditor = new UUIDEditor(element); + + it('returns the UUID string with hyphens (no byte reversal)', function () { + expect(uuidEditor.value()).to.equal(uuidString); + }); + }); + + context('when the value is already a string', function () { + const element = new Element('uuid', uuidString, false); + element.currentType = 'UUID'; + const uuidEditor = new UUIDEditor(element); + + it('returns the string as-is', function () { + expect(uuidEditor.value()).to.equal(uuidString); + }); + }); + }); + + describe('#edit', function () { + context('when the UUID string is valid', function () { + const binary = Binary.createFromHexString(uuidHex, Binary.SUBTYPE_UUID); + const element = new Element('uuid', binary, false); + element.currentType = 'UUID'; + const uuidEditor = new UUIDEditor(element); + const newValidString = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'; + + before(function () { + uuidEditor.start(); + uuidEditor.edit(newValidString); + }); + + it('keeps the string as the current value', function () { + expect(element.currentValue).to.equal(newValidString); + }); + + it('sets the current value as valid', function () { + expect(element.isCurrentTypeValid()).to.equal(true); + }); + }); + + context('when the UUID string is invalid', function () { + const binary = Binary.createFromHexString(uuidHex, Binary.SUBTYPE_UUID); + const element = new Element('uuid', binary, false); + element.currentType = 'UUID'; + const uuidEditor = new UUIDEditor(element); + const invalidString = 'not-a-valid-uuid'; + + before(function () { + uuidEditor.start(); + uuidEditor.edit(invalidString); + }); + + it('keeps the string as the current value', function () { + expect(element.currentValue).to.equal(invalidString); + }); + + it('sets the current value as invalid', function () { + expect(element.isCurrentTypeValid()).to.equal(false); + }); + + it('sets the invalid message', function () { + expect(element.invalidTypeMessage).to.include('not a valid UUID'); + }); + }); + }); + + describe('#complete', function () { + context('when the UUID string is valid', function () { + const binary = Binary.createFromHexString(uuidHex, Binary.SUBTYPE_UUID); + const element = new Element('uuid', binary, false); + element.currentType = 'UUID'; + const uuidEditor = new UUIDEditor(element); + const newValidString = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'; + + before(function () { + uuidEditor.start(); + uuidEditor.edit(newValidString); + uuidEditor.complete(); + }); + + it('casts the current value to Binary', function () { + expect(element.currentValue).to.be.instanceOf(Binary); + expect((element.currentValue as Binary).sub_type).to.equal( + Binary.SUBTYPE_UUID + ); + }); + + it('sets the current value as valid', function () { + expect(element.isCurrentTypeValid()).to.equal(true); + }); + }); + + context('when the UUID string is invalid', function () { + const binary = Binary.createFromHexString(uuidHex, Binary.SUBTYPE_UUID); + const element = new Element('uuid', binary, false); + element.currentType = 'UUID'; + const uuidEditor = new UUIDEditor(element); + const invalidString = 'nope'; + + before(function () { + uuidEditor.start(); + uuidEditor.edit(invalidString); + uuidEditor.complete(); + }); + + it('keeps the string as the current value', function () { + expect(element.currentValue).to.equal('nope'); + }); + + it('sets the current value as invalid', function () { + expect(element.isCurrentTypeValid()).to.equal(false); + }); + }); + }); + + describe('#start', function () { + context('when the current type is valid', function () { + const binary = Binary.createFromHexString(uuidHex, Binary.SUBTYPE_UUID); + const element = new Element('uuid', binary, false); + element.currentType = 'UUID'; + const uuidEditor = new UUIDEditor(element); + + before(function () { + uuidEditor.start(); + }); + + it('converts the Binary to a UUID string for editing', function () { + expect(element.currentValue).to.equal(uuidString); + }); + + it('sets the current value as valid', function () { + expect(element.isCurrentTypeValid()).to.equal(true); + }); + }); + }); +}); diff --git a/packages/hadron-type-checker/src/index.ts b/packages/hadron-type-checker/src/index.ts index 7b7cb12810e..3338d6481f9 100644 --- a/packages/hadron-type-checker/src/index.ts +++ b/packages/hadron-type-checker/src/index.ts @@ -1,2 +1,2 @@ -export { default } from './type-checker'; +export { default, UUID_REGEX } from './type-checker'; export type { TypeCastMap, TypeCastTypes } from './type-checker'; diff --git a/packages/hadron-type-checker/src/type-checker.ts b/packages/hadron-type-checker/src/type-checker.ts index afa188945cf..3669114ab24 100644 --- a/packages/hadron-type-checker/src/type-checker.ts +++ b/packages/hadron-type-checker/src/type-checker.ts @@ -23,6 +23,7 @@ import { Code, BSONSymbol, Timestamp, + UUID, } from 'bson'; export type TypeCastMap = { @@ -45,6 +46,10 @@ export type TypeCastMap = { BSONSymbol: BSONSymbol; Timestamp: Timestamp; Undefined: undefined; + UUID: Binary; + LegacyJavaUUID: Binary; + LegacyCSharpUUID: Binary; + LegacyPythonUUID: Binary; }; export type TypeCastTypes = keyof TypeCastMap; @@ -254,6 +259,138 @@ const toBinary = (object: unknown): Binary => { return new Binary(buffer, Binary.SUBTYPE_DEFAULT); }; +/** + * UUID regex pattern for validation (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx). + */ +export const 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: string): void => { + if (!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: string): string => { + return uuidString.replace(/-/g, ''); +}; + +/** + * Generates a random UUID string in the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. + */ +const generateRandomUUID = (): string => { + return new UUID().toString(); +}; + +/** + * Converts to UUID (Binary subtype 4). + * If the input is empty, generates a random UUID. + */ +const toUUID = (object: unknown): Binary => { + const uuidString = toString(object).trim(); + if (!uuidString) { + return new UUID().toBinary(); + } + validateUUIDString(uuidString); + const hex = uuidStringToHex(uuidString); + return Binary.createFromHexString(hex, 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. + */ +const toLegacyJavaUUID = (object: unknown): Binary => { + let uuidString = toString(object).trim(); + if (!uuidString) { + uuidString = generateRandomUUID(); + } else { + validateUUIDString(uuidString); + } + const hex = uuidStringToHex(uuidString); + + // Reverse byte order for Java legacy UUID format. + // Split into MSB (first 16 hex chars / 8 bytes) and LSB (last 16 hex chars / 8 bytes). + let msb = hex.substring(0, 16); + let lsb = hex.substring(16, 32); + + // Reverse pairs of hex characters (bytes) for both MSB and LSB. + 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 Binary.createFromHexString(msb + lsb, 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. + */ +const toLegacyCSharpUUID = (object: unknown): Binary => { + let uuidString = toString(object).trim(); + if (!uuidString) { + uuidString = generateRandomUUID(); + } else { + validateUUIDString(uuidString); + } + const hex = uuidStringToHex(uuidString); + + // Reverse byte order for C# legacy UUID format (first 3 groups only). + // Group a: first 4 bytes (8 hex chars), group b: next 2 bytes (4 hex chars), + // group c: next 2 bytes (4 hex chars), group d: remaining 8 bytes (16 hex chars). + 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 Binary.createFromHexString(a + b + c + d, 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. + */ +const toLegacyPythonUUID = (object: unknown): Binary => { + let uuidString = toString(object).trim(); + if (!uuidString) { + uuidString = generateRandomUUID(); + } else { + validateUUIDString(uuidString); + } + const hex = uuidStringToHex(uuidString); + return Binary.createFromHexString(hex, Binary.SUBTYPE_UUID_OLD); +}; + const toRegex = (object: unknown): BSONRegExp => { return new BSONRegExp(toString(object)); }; @@ -296,6 +433,10 @@ const CASTERS: { BSONSymbol: toSymbol, Timestamp: toTimestamp, Undefined: toUndefined, + UUID: toUUID, + LegacyJavaUUID: toLegacyJavaUUID, + LegacyCSharpUUID: toLegacyCSharpUUID, + LegacyPythonUUID: toLegacyPythonUUID, }; /** @@ -367,8 +508,17 @@ class TypeChecker { /** * 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: unknown): TypeCastTypes { + type( + object: unknown, + legacyUUIDEncoding?: + | 'LegacyJavaUUID' + | 'LegacyCSharpUUID' + | 'LegacyPythonUUID' + ): TypeCastTypes { if (hasIn(object, BSON_TYPE)) { const bsonObj = object as { _bsontype: string }; if (bsonObj._bsontype === LONG) { @@ -380,6 +530,20 @@ class TypeChecker { if (bsonObj._bsontype === SYMBOL) { return 'BSONSymbol'; } + // Handle Binary UUID subtypes + if (bsonObj._bsontype === 'Binary') { + const binary = object as Binary; + if (binary.sub_type === Binary.SUBTYPE_UUID) { + return 'UUID'; + } + if ( + binary.sub_type === Binary.SUBTYPE_UUID_OLD && + binary.buffer.length === 16 && + legacyUUIDEncoding + ) { + return legacyUUIDEncoding; + } + } return bsonObj._bsontype as TypeCastTypes; } if (isNumber(object)) { diff --git a/packages/hadron-type-checker/test/type-checker.test.ts b/packages/hadron-type-checker/test/type-checker.test.ts index 1cc4ec23eb0..e9f53ca4171 100644 --- a/packages/hadron-type-checker/test/type-checker.test.ts +++ b/packages/hadron-type-checker/test/type-checker.test.ts @@ -801,6 +801,10 @@ describe('TypeChecker', function () { 'BSONSymbol', 'Timestamp', 'Undefined', + 'UUID', + 'LegacyJavaUUID', + 'LegacyCSharpUUID', + 'LegacyPythonUUID', ]); }); }); @@ -826,6 +830,10 @@ describe('TypeChecker', function () { 'BSONSymbol', 'Timestamp', 'Undefined', + 'UUID', + 'LegacyJavaUUID', + 'LegacyCSharpUUID', + 'LegacyPythonUUID', ]); }); }); From 6f5f20603b4312c77a921ebeb34a8900d2eb3a40 Mon Sep 17 00:00:00 2001 From: Ivan Medina Date: Wed, 4 Feb 2026 10:50:26 -0300 Subject: [PATCH 02/15] fix: add UUID types to UNCASTED_EMPTY_TYPE_VALUE --- packages/hadron-document/src/utils.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/hadron-document/src/utils.ts b/packages/hadron-document/src/utils.ts index a807632e8c5..7033bcd3af5 100644 --- a/packages/hadron-document/src/utils.ts +++ b/packages/hadron-document/src/utils.ts @@ -24,6 +24,10 @@ const UNCASTED_EMPTY_TYPE_VALUE: { Boolean: false, Undefined: undefined, Null: null, + UUID: '', + LegacyJavaUUID: '', + LegacyCSharpUUID: '', + LegacyPythonUUID: '', }; const maxFourYearDate = new Date('9999-12-31T23:59:59.999Z').valueOf(); From ce6d9053417d5b1caba9eb4b8473a1e95523eb94 Mon Sep 17 00:00:00 2001 From: Ivan Medina Date: Wed, 4 Feb 2026 11:08:08 -0300 Subject: [PATCH 03/15] fix: remove unused UUID_REGEX import --- .../src/components/document-list/element-editors.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compass-components/src/components/document-list/element-editors.tsx b/packages/compass-components/src/components/document-list/element-editors.tsx index 45a896db79b..a74df794ae4 100644 --- a/packages/compass-components/src/components/document-list/element-editors.tsx +++ b/packages/compass-components/src/components/document-list/element-editors.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from 'react'; import type { Element as HadronElementType } from 'hadron-document'; import type { TypeCastMap } from 'hadron-type-checker'; -import TypeChecker, { UUID_REGEX } from 'hadron-type-checker'; +import TypeChecker from 'hadron-type-checker'; import { css, cx } from '@leafygreen-ui/emotion'; import { palette } from '@leafygreen-ui/palette'; import { spacing } from '@leafygreen-ui/tokens'; From d36e7111bbec6183b4790c860be22aee8a577acf Mon Sep 17 00:00:00 2001 From: Ivan Medina Date: Wed, 4 Feb 2026 11:38:40 -0300 Subject: [PATCH 04/15] fix: update test fixtures to use $uuid format for Binary subtype 04 --- .../docs/all-bson-types.exported.default.ejson | 5 +---- .../test/json/good.exported.ejson | 15 +++------------ 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/packages/compass-import-export/test/docs/all-bson-types.exported.default.ejson b/packages/compass-import-export/test/docs/all-bson-types.exported.default.ejson index bfce47d06c8..3e890070ec3 100644 --- a/packages/compass-import-export/test/docs/all-bson-types.exported.default.ejson +++ b/packages/compass-import-export/test/docs/all-bson-types.exported.default.ejson @@ -91,10 +91,7 @@ } }, "uuid": { - "$binary": { - "base64": "qqqqqqqqSqqqqqqqqqqqqg==", - "subType": "04" - } + "$uuid": "aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaaaa" }, "md5": { "$binary": { diff --git a/packages/compass-import-export/test/json/good.exported.ejson b/packages/compass-import-export/test/json/good.exported.ejson index 79987beccbd..5737be82535 100644 --- a/packages/compass-import-export/test/json/good.exported.ejson +++ b/packages/compass-import-export/test/json/good.exported.ejson @@ -3,10 +3,7 @@ "$oid": "63f656c128ff7868ed701ca3" }, "uuid": { - "$binary": { - "base64": "ACY24RDNTIuppwG3v9OJnA==", - "subType": "04" - } + "$uuid": "002636e1-10cd-4c8b-a9a7-01b7bfd3899c" }, "name": "Arlo" }, @@ -15,10 +12,7 @@ "$oid": "63f656c128ff7868ed701ca4" }, "uuid": { - "$binary": { - "base64": "ADH2ysDsTLCmbB/hTKKcGA==", - "subType": "04" - } + "$uuid": "0031f6ca-c0ec-4cb0-a66c-1fe14ca29c18" }, "name": "Basil" }, @@ -27,10 +21,7 @@ "$oid": "63f656c128ff7868ed701ca5" }, "uuid": { - "$binary": { - "base64": "ADVkMBdRSZauRXdnZILiEA==", - "subType": "04" - } + "$uuid": "00356430-1751-4996-ae45-77676482e210" }, "name": "Kochka" }] \ No newline at end of file From 382a64c27083b8230d35d495d139628de1781df3 Mon Sep 17 00:00:00 2001 From: Ivan Medina Date: Thu, 5 Feb 2026 10:47:07 -0300 Subject: [PATCH 05/15] refactor: extract UUID byte reversal logic into shared utilities Address code review feedback by extracting duplicated byte reversal logic into shared utility functions in hadron-type-checker: - uuidHexToString: converts hex string to UUID format with hyphens - reverseJavaUUIDBytes: reverses byte order for Java legacy UUID format - reverseCSharpUUIDBytes: reverses byte order for C# legacy UUID format These utilities are now used in both type-checker.ts and uuid.ts, eliminating code duplication and improving maintainability. --- packages/hadron-document/src/editor/uuid.ts | 60 ++-------- packages/hadron-type-checker/src/index.ts | 8 +- .../hadron-type-checker/src/type-checker.ts | 106 +++++++++++------- 3 files changed, 82 insertions(+), 92 deletions(-) diff --git a/packages/hadron-document/src/editor/uuid.ts b/packages/hadron-document/src/editor/uuid.ts index 5efd4580245..4082fda5cbf 100644 --- a/packages/hadron-document/src/editor/uuid.ts +++ b/packages/hadron-document/src/editor/uuid.ts @@ -1,4 +1,8 @@ -import TypeChecker from 'hadron-type-checker'; +import TypeChecker, { + uuidHexToString, + reverseJavaUUIDBytes, + reverseCSharpUUIDBytes, +} from 'hadron-type-checker'; import { Binary } from 'bson'; import { ElementEvents } from '../element-events'; import StandardEditor from './standard'; @@ -6,23 +10,6 @@ import type { Element } from '../element'; import { UUID_TYPES, type UUIDType } from '../element'; import type { BSONValue } from '../utils'; -/** - * Converts a hex string to UUID format with hyphens. - */ -const toUUIDWithHyphens = (hex: string): string => { - return ( - hex.substring(0, 8) + - '-' + - hex.substring(8, 12) + - '-' + - hex.substring(12, 16) + - '-' + - hex.substring(16, 20) + - '-' + - hex.substring(20, 32) - ); -}; - /** * Converts a Binary UUID (subtype 4) to a UUID string with hyphens. */ @@ -41,28 +28,8 @@ const binaryToUUIDString = (binary: Binary): string => { */ const binaryToLegacyJavaUUIDString = (binary: Binary): string => { const hex = Buffer.from(binary.buffer).toString('hex'); - let msb = hex.substring(0, 16); - let lsb = hex.substring(16, 32); - // Reverse pairs of hex characters (bytes) for both MSB and LSB. - 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 toUUIDWithHyphens(msb + lsb); + const reversedHex = reverseJavaUUIDBytes(hex); + return uuidHexToString(reversedHex); }; /** @@ -71,15 +38,8 @@ const binaryToLegacyJavaUUIDString = (binary: Binary): string => { */ const binaryToLegacyCSharpUUIDString = (binary: Binary): string => { const hex = Buffer.from(binary.buffer).toString('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 toUUIDWithHyphens(a + b + c + d); + const reversedHex = reverseCSharpUUIDBytes(hex); + return uuidHexToString(reversedHex); }; /** @@ -88,7 +48,7 @@ const binaryToLegacyCSharpUUIDString = (binary: Binary): string => { */ const binaryToLegacyPythonUUIDString = (binary: Binary): string => { const hex = Buffer.from(binary.buffer).toString('hex'); - return toUUIDWithHyphens(hex); + return uuidHexToString(hex); }; /** diff --git a/packages/hadron-type-checker/src/index.ts b/packages/hadron-type-checker/src/index.ts index 3338d6481f9..4b8a6ba2140 100644 --- a/packages/hadron-type-checker/src/index.ts +++ b/packages/hadron-type-checker/src/index.ts @@ -1,2 +1,8 @@ -export { default, UUID_REGEX } from './type-checker'; +export { + default, + UUID_REGEX, + uuidHexToString, + reverseJavaUUIDBytes, + reverseCSharpUUIDBytes, +} from './type-checker'; export type { TypeCastMap, TypeCastTypes } from './type-checker'; diff --git a/packages/hadron-type-checker/src/type-checker.ts b/packages/hadron-type-checker/src/type-checker.ts index 3669114ab24..bc8145a2ecb 100644 --- a/packages/hadron-type-checker/src/type-checker.ts +++ b/packages/hadron-type-checker/src/type-checker.ts @@ -283,6 +283,67 @@ const uuidStringToHex = (uuidString: string): string => { return uuidString.replace(/-/g, ''); }; +/** + * Converts a hex string (without hyphens) to UUID format with hyphens. + */ +export const uuidHexToString = (hex: string): string => { + return ( + hex.substring(0, 8) + + '-' + + hex.substring(8, 12) + + '-' + + hex.substring(12, 16) + + '-' + + hex.substring(16, 20) + + '-' + + hex.substring(20, 32) + ); +}; + +/** + * 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. + */ +export const reverseJavaUUIDBytes = (hex: string): string => { + 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; +}; + +/** + * 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. + */ +export const reverseCSharpUUIDBytes = (hex: string): string => { + 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; +}; + /** * Generates a random UUID string in the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. */ @@ -317,33 +378,8 @@ const toLegacyJavaUUID = (object: unknown): Binary => { validateUUIDString(uuidString); } const hex = uuidStringToHex(uuidString); - - // Reverse byte order for Java legacy UUID format. - // Split into MSB (first 16 hex chars / 8 bytes) and LSB (last 16 hex chars / 8 bytes). - let msb = hex.substring(0, 16); - let lsb = hex.substring(16, 32); - - // Reverse pairs of hex characters (bytes) for both MSB and LSB. - 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 Binary.createFromHexString(msb + lsb, Binary.SUBTYPE_UUID_OLD); + const reversedHex = reverseJavaUUIDBytes(hex); + return Binary.createFromHexString(reversedHex, Binary.SUBTYPE_UUID_OLD); }; /** @@ -359,20 +395,8 @@ const toLegacyCSharpUUID = (object: unknown): Binary => { validateUUIDString(uuidString); } const hex = uuidStringToHex(uuidString); - - // Reverse byte order for C# legacy UUID format (first 3 groups only). - // Group a: first 4 bytes (8 hex chars), group b: next 2 bytes (4 hex chars), - // group c: next 2 bytes (4 hex chars), group d: remaining 8 bytes (16 hex chars). - 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 Binary.createFromHexString(a + b + c + d, Binary.SUBTYPE_UUID_OLD); + const reversedHex = reverseCSharpUUIDBytes(hex); + return Binary.createFromHexString(reversedHex, Binary.SUBTYPE_UUID_OLD); }; /** From 348e60d881470de40a455fdc9b2fa5ba145e846a Mon Sep 17 00:00:00 2001 From: Ivan Medina Date: Thu, 5 Feb 2026 10:50:28 -0300 Subject: [PATCH 06/15] perf: memoize UUID input style to prevent unnecessary re-renders Address code review feedback by using useMemo for the UUID input style object instead of creating it inline on every render. --- .../src/components/document-list/element-editors.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/compass-components/src/components/document-list/element-editors.tsx b/packages/compass-components/src/components/document-list/element-editors.tsx index a74df794ae4..a8a7bcba750 100644 --- a/packages/compass-components/src/components/document-list/element-editors.tsx +++ b/packages/compass-components/src/components/document-list/element-editors.tsx @@ -250,6 +250,11 @@ export const ValueEditor: React.FunctionComponent<{ return { width: `${Math.max(val.length, 1)}ch` }; }, [val, type]); + const uuidInputStyle = useMemo(() => { + // UUID format is 36 characters (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) + return { width: `${Math.max(val.length, 36)}ch` }; + }, [val]); + return ( <> {editing ? ( @@ -323,7 +328,7 @@ export const ValueEditor: React.FunctionComponent<{ : editorInvalidLightMode) )} spellCheck="false" - style={{ width: `${Math.max(val.length, 36)}ch` }} + style={uuidInputStyle} {...(mergedProps as React.HTMLProps)} > ') From 118b901be0c3bfc46aa5788a7617264c18224c57 Mon Sep 17 00:00:00 2001 From: Ivan Medina Date: Fri, 6 Feb 2026 08:33:05 -0300 Subject: [PATCH 07/15] fix: properly convert between UUID types in changeType When changing from one UUID type to another (e.g., LegacyCSharpUUID to LegacyJavaUUID), the system was producing corrupted data because toString() was being called on Binary objects. This fix: - Adds convertBinaryUUID() function that properly handles encoding/decoding - Updates changeType() to detect UUID-to-UUID conversions and use the special conversion function - Ensures the Binary is first decoded using the source encoding, then re-encoded using the target encoding --- packages/hadron-document/src/element.ts | 21 ++- packages/hadron-type-checker/src/index.ts | 1 + .../hadron-type-checker/src/type-checker.ts | 128 +++++++++++++++++- 3 files changed, 143 insertions(+), 7 deletions(-) diff --git a/packages/hadron-document/src/element.ts b/packages/hadron-document/src/element.ts index 8a7ece467ef..524e44b3b05 100644 --- a/packages/hadron-document/src/element.ts +++ b/packages/hadron-document/src/element.ts @@ -4,13 +4,13 @@ import EventEmitter from 'eventemitter3'; import { isPlainObject, isArray, isEqual, isString } from 'lodash'; import type { ObjectGeneratorOptions } from './object-generator'; import ObjectGenerator from './object-generator'; -import TypeChecker from 'hadron-type-checker'; -import { UUID } from 'bson'; +import TypeChecker, { convertBinaryUUID } from 'hadron-type-checker'; +import { Binary, UUID } from 'bson'; import DateEditor from './editor/date'; import { ElementEvents, type ElementEventsType } from './element-events'; import type { Document } from './document'; import type { TypeCastTypes } from 'hadron-type-checker'; -import type { Binary, ObjectId } from 'bson'; +import type { ObjectId } from 'bson'; import type { BSONArray, BSONObject, @@ -257,6 +257,21 @@ export class Element extends EventEmitter { const editor = new DateEditor(this); editor.edit(this.generateObject()); editor.complete(); + } else if ( + (UUID_TYPES as readonly string[]).includes(newType) && + (UUID_TYPES as readonly string[]).includes(this.currentType) && + this.currentValue instanceof Binary + ) { + // Special handling for converting between UUID types + // We need to use the source type to properly decode the binary + const convertedBinary = convertBinaryUUID( + this.currentValue, + this.currentType, + newType + ); + this.edit(convertedBinary); + this.currentType = newType; + this._bubbleUp(ElementEvents.Edited, this); } else { this.edit(TypeChecker.cast(this.generateObject(), newType)); // For UUID types, explicitly set the currentType since TypeChecker.type() diff --git a/packages/hadron-type-checker/src/index.ts b/packages/hadron-type-checker/src/index.ts index 4b8a6ba2140..d1328309974 100644 --- a/packages/hadron-type-checker/src/index.ts +++ b/packages/hadron-type-checker/src/index.ts @@ -4,5 +4,6 @@ export { uuidHexToString, reverseJavaUUIDBytes, reverseCSharpUUIDBytes, + convertBinaryUUID, } from './type-checker'; export type { TypeCastMap, TypeCastTypes } from './type-checker'; diff --git a/packages/hadron-type-checker/src/type-checker.ts b/packages/hadron-type-checker/src/type-checker.ts index bc8145a2ecb..3d893cb6e1f 100644 --- a/packages/hadron-type-checker/src/type-checker.ts +++ b/packages/hadron-type-checker/src/type-checker.ts @@ -351,12 +351,129 @@ const generateRandomUUID = (): string => { return new UUID().toString(); }; +/** + * Extracts the raw hex string from a Binary UUID (subtype 3 or 4). + * Returns the hex without any byte order reversal. + */ +const binaryToRawHex = (binary: Binary): string => { + return Buffer.from(binary.buffer).toString('hex'); +}; + +/** + * 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: Binary, + sourceEncoding?: 'Java' | 'CSharp' | 'Python' +): string => { + const hex = binaryToRawHex(binary); + + // For standard UUID (subtype 4), no byte reversal needed + if (binary.sub_type === Binary.SUBTYPE_UUID) { + return 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 uuidHexToString(reverseJavaUUIDBytes(hex)); + case 'CSharp': + // Reverse C# encoding to get standard UUID + return uuidHexToString(reverseCSharpUUIDBytes(hex)); + case 'Python': + default: + // Python uses standard byte order, no reversal needed + return 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: unknown, + sourceEncoding?: 'Java' | 'CSharp' | 'Python' +): string => { + if (object instanceof Binary) { + if ( + object.sub_type === Binary.SUBTYPE_UUID || + object.sub_type === Binary.SUBTYPE_UUID_OLD + ) { + return binaryToUUIDStringWithEncoding(object, sourceEncoding); + } + } + return toString(object).trim(); +}; + +/** + * Mapping from UUID type names to encoding names. + */ +const UUID_TYPE_TO_ENCODING: Record< + string, + 'Java' | 'CSharp' | 'Python' | undefined +> = { + 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 + */ +export const convertBinaryUUID = ( + binary: Binary, + sourceType: string, + targetType: string +): Binary => { + // 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 Binary.createFromHexString(hex, Binary.SUBTYPE_UUID); + case 'LegacyJavaUUID': + return Binary.createFromHexString( + reverseJavaUUIDBytes(hex), + Binary.SUBTYPE_UUID_OLD + ); + case 'LegacyCSharpUUID': + return Binary.createFromHexString( + reverseCSharpUUIDBytes(hex), + Binary.SUBTYPE_UUID_OLD + ); + case 'LegacyPythonUUID': + return Binary.createFromHexString(hex, Binary.SUBTYPE_UUID_OLD); + default: + throw new Error(`Unknown UUID type: ${targetType}`); + } +}; + /** * 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: unknown): Binary => { - const uuidString = toString(object).trim(); + const uuidString = getUUIDStringFromObject(object); if (!uuidString) { return new UUID().toBinary(); } @@ -369,9 +486,10 @@ const toUUID = (object: unknown): Binary => { * 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: unknown): Binary => { - let uuidString = toString(object).trim(); + let uuidString = getUUIDStringFromObject(object, 'Java'); if (!uuidString) { uuidString = generateRandomUUID(); } else { @@ -386,9 +504,10 @@ const toLegacyJavaUUID = (object: unknown): Binary => { * 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: unknown): Binary => { - let uuidString = toString(object).trim(); + let uuidString = getUUIDStringFromObject(object, 'CSharp'); if (!uuidString) { uuidString = generateRandomUUID(); } else { @@ -403,9 +522,10 @@ const toLegacyCSharpUUID = (object: unknown): Binary => { * 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: unknown): Binary => { - let uuidString = toString(object).trim(); + let uuidString = getUUIDStringFromObject(object, 'Python'); if (!uuidString) { uuidString = generateRandomUUID(); } else { From 66986d3732ec8494a078d29e7fd700ba86881cf9 Mon Sep 17 00:00:00 2001 From: Ivan Medina Date: Fri, 6 Feb 2026 09:27:47 -0300 Subject: [PATCH 08/15] fix: use Object.create(null) for UUID_TYPE_TO_ENCODING record --- packages/hadron-type-checker/src/type-checker.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/hadron-type-checker/src/type-checker.ts b/packages/hadron-type-checker/src/type-checker.ts index 3d893cb6e1f..317ec725ecb 100644 --- a/packages/hadron-type-checker/src/type-checker.ts +++ b/packages/hadron-type-checker/src/type-checker.ts @@ -417,12 +417,12 @@ const getUUIDStringFromObject = ( const UUID_TYPE_TO_ENCODING: Record< string, 'Java' | 'CSharp' | 'Python' | undefined -> = { +> = Object.assign(Object.create(null), { UUID: undefined, LegacyJavaUUID: 'Java', LegacyCSharpUUID: 'CSharp', LegacyPythonUUID: 'Python', -}; +}); /** * Converts a Binary UUID from one encoding to another. From 824f83058f227a24e49d18abced88d0dd7466126 Mon Sep 17 00:00:00 2001 From: Ivan Medina Date: Fri, 6 Feb 2026 11:49:29 -0300 Subject: [PATCH 09/15] refactor: use shared UUID utilities from hadron-type-checker in bson-value.tsx Removes duplicated byte reversal logic (toUUIDWithHyphens, toLegacyJavaUUID, toLegacyCSharpUUID, toLegacyPythonUUID) and uses the shared utilities (uuidHexToString, reverseJavaUUIDBytes, reverseCSharpUUIDBytes) from hadron-type-checker instead. --- .../src/components/bson-value.tsx | 63 +++---------------- 1 file changed, 10 insertions(+), 53 deletions(-) diff --git a/packages/compass-components/src/components/bson-value.tsx b/packages/compass-components/src/components/bson-value.tsx index 51cb3291c53..df1689bfd14 100644 --- a/packages/compass-components/src/components/bson-value.tsx +++ b/packages/compass-components/src/components/bson-value.tsx @@ -1,5 +1,10 @@ import React, { useMemo } from 'react'; import type { TypeCastMap } from 'hadron-type-checker'; +import { + uuidHexToString, + reverseJavaUUIDBytes, + reverseCSharpUUIDBytes, +} from 'hadron-type-checker'; import { Binary } from 'bson'; import type { DBRef } from 'bson'; import { variantColors } from '@leafygreen-ui/code'; @@ -125,69 +130,21 @@ const ObjectIdValue: React.FunctionComponent> = ({ ); }; -const toUUIDWithHyphens = (hex: string): string => { - return ( - hex.substring(0, 8) + - '-' + - hex.substring(8, 12) + - '-' + - hex.substring(12, 16) + - '-' + - hex.substring(16, 20) + - '-' + - hex.substring(20, 32) - ); -}; - const toLegacyJavaUUID = ({ value }: PropsByValueType<'Binary'>) => { - // Get the hex representation from the buffer. const hex = Buffer.from(value.buffer).toString('hex'); - // Reverse byte order for Java legacy UUID format (reverse all bytes). - let msb = hex.substring(0, 16); - let lsb = hex.substring(16, 32); - // Reverse pairs of hex characters (bytes). - 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); - const uuid = msb + lsb; - return "LegacyJavaUUID('" + toUUIDWithHyphens(uuid) + "')"; + const reversedHex = reverseJavaUUIDBytes(hex); + return "LegacyJavaUUID('" + uuidHexToString(reversedHex) + "')"; }; const toLegacyCSharpUUID = ({ value }: PropsByValueType<'Binary'>) => { - // Get the hex representation from the buffer. const hex = Buffer.from(value.buffer).toString('hex'); - // Reverse byte order for C# legacy UUID format (first 3 groups only). - 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); - const uuid = a + b + c + d; - return "LegacyCSharpUUID('" + toUUIDWithHyphens(uuid) + "')"; + const reversedHex = reverseCSharpUUIDBytes(hex); + return "LegacyCSharpUUID('" + uuidHexToString(reversedHex) + "')"; }; const toLegacyPythonUUID = ({ value }: PropsByValueType<'Binary'>) => { - // Get the hex representation from the buffer. const hex = Buffer.from(value.buffer).toString('hex'); - return "LegacyPythonUUID('" + toUUIDWithHyphens(hex) + "')"; + return "LegacyPythonUUID('" + uuidHexToString(hex) + "')"; }; // Binary sub_type 3. From 18847e49de8469c20016a237e37de634aee0309c Mon Sep 17 00:00:00 2001 From: Ivan Medina Date: Mon, 9 Feb 2026 09:53:25 -0300 Subject: [PATCH 10/15] feat: enable editing of legacy UUID values in document editor - Modified isValueEditable() in element.ts to allow editing Binary UUIDs (subtype 3 and 4) - Updated useElementEditor() in element.tsx to use display type for editor selection - Updated UUIDEditor constructor to accept displayType parameter - Modified UUIDEditor start() and complete() methods to preserve UUID type during editing --- .../src/components/document-list/element.tsx | 22 ++++++++++------- .../hadron-document/src/editor/standard.ts | 5 ++-- packages/hadron-document/src/editor/uuid.ts | 24 +++++++++++++++---- packages/hadron-document/src/element.ts | 12 +++++++++- 4 files changed, 47 insertions(+), 16 deletions(-) diff --git a/packages/compass-components/src/components/document-list/element.tsx b/packages/compass-components/src/components/document-list/element.tsx index 5c91012d087..86b1727452b 100644 --- a/packages/compass-components/src/components/document-list/element.tsx +++ b/packages/compass-components/src/components/document-list/element.tsx @@ -48,18 +48,23 @@ function getEditorByType(type: HadronElementType['type']) { } } -function useElementEditor(el: HadronElementType) { +function useElementEditor( + el: HadronElementType, + displayType: HadronElementType['type'] +) { return useMemo( () => { - const Editor = getEditorByType(el.currentType); - return new Editor(el); + // Use the display type for editor selection - this ensures that Binary UUIDs + // get the UUIDEditor even when el.currentType is 'Binary' + const Editor = getEditorByType(displayType); + return new Editor(el, displayType); }, - // The list of deps is exhaustive, but we want `currentType` to be an + // The list of deps is exhaustive, but we want `displayType` to be an // explicit dependency of the memo to make sure that even if the `el` - // instance is the same, but `currentType` changed, we create a new editor + // instance is the same, but `displayType` changed, we create a new editor // instance // eslint-disable-next-line react-hooks/exhaustive-deps - [el, el.currentType] + [el, displayType] ); } @@ -104,7 +109,8 @@ function getDisplayType( function useHadronElement(el: HadronElementType) { const forceUpdate = useForceUpdate(); const legacyUUIDEncoding = useLegacyUUIDDisplayContext(); - const editor = useElementEditor(el); + const displayType = getDisplayType(el, legacyUUIDEncoding); + const editor = useElementEditor(el, displayType); // NB: Duplicate key state is kept local to the component and not derived on // every change so that only the changed key is highlighed as duplicate const [isDuplicateKey, setIsDuplicateKey] = useState(() => { @@ -209,7 +215,7 @@ function useHadronElement(el: HadronElementType) { completeEdit: editor.complete.bind(editor), }, type: { - value: getDisplayType(el, legacyUUIDEncoding), + value: displayType, change(newVal: HadronElementType['type']) { el.changeType(newVal); }, diff --git a/packages/hadron-document/src/editor/standard.ts b/packages/hadron-document/src/editor/standard.ts index 22a8fbf3c26..b4c04fb5050 100644 --- a/packages/hadron-document/src/editor/standard.ts +++ b/packages/hadron-document/src/editor/standard.ts @@ -21,9 +21,10 @@ export default class StandardEditor { /** * Create the editor with the element. * - * @param {Element} element - The hadron document element. + * @param element - The hadron document element. + * @param _displayType - Optional display type (used by subclasses like UUIDEditor). */ - constructor(element: Element) { + constructor(element: Element, _displayType?: string) { this.element = element; this.type = element.currentType; this.editing = false; diff --git a/packages/hadron-document/src/editor/uuid.ts b/packages/hadron-document/src/editor/uuid.ts index 4082fda5cbf..0f686900265 100644 --- a/packages/hadron-document/src/editor/uuid.ts +++ b/packages/hadron-document/src/editor/uuid.ts @@ -57,12 +57,19 @@ const binaryToLegacyPythonUUIDString = (binary: Binary): string => { export default class UUIDEditor extends StandardEditor { uuidType: UUIDType; - constructor(element: Element) { + /** + * Create the UUID editor. + * + * @param element - The hadron document element. + * @param displayType - Optional display type override. Used when element.currentType + * is 'Binary' but the element should be treated as a UUID type. + */ + constructor(element: Element, displayType?: string) { super(element); - this.uuidType = (UUID_TYPES as readonly string[]).includes( - element.currentType - ) - ? (element.currentType as UUIDType) + // Use displayType if provided and it's a UUID type, otherwise fall back to element.currentType + const effectiveType = displayType ?? element.currentType; + this.uuidType = (UUID_TYPES as readonly string[]).includes(effectiveType) + ? (effectiveType as UUIDType) : 'UUID'; } @@ -112,6 +119,10 @@ export default class UUIDEditor extends StandardEditor { start(): void { super.start(); if (this.element.isCurrentTypeValid()) { + // Update the currentType to the UUID type so the UI displays correctly + // This is needed because the element may have been created with currentType='Binary' + // but we want to edit it as a UUID type + this.element.currentType = this.uuidType; this.edit(this.value()); } } @@ -125,6 +136,9 @@ export default class UUIDEditor extends StandardEditor { this.element.edit( TypeChecker.cast(this.element.currentValue, this.uuidType) ); + // Preserve the UUID type since element.edit() sets currentType to 'Binary' + // for Binary values (TypeChecker.type() returns 'Binary' for Binary objects) + this.element.currentType = this.uuidType; } } } diff --git a/packages/hadron-document/src/element.ts b/packages/hadron-document/src/element.ts index 524e44b3b05..7a6c7e59a89 100644 --- a/packages/hadron-document/src/element.ts +++ b/packages/hadron-document/src/element.ts @@ -689,9 +689,19 @@ export class Element extends EventEmitter { const isUUIDType = (UUID_TYPES as readonly string[]).includes( this.currentType ); + // Also check for Binary values that are actually UUIDs (subtype 3 or 4) + // This handles the case where currentType is 'Binary' but it's actually a UUID + const isBinaryUUID = + this.currentType === 'Binary' && + this.currentValue instanceof Binary && + (this.currentValue.sub_type === Binary.SUBTYPE_UUID || + (this.currentValue.sub_type === Binary.SUBTYPE_UUID_OLD && + this.currentValue.buffer.length === 16)); return ( this._isKeyLegallyEditable() && - (isUUIDType || !UNEDITABLE_TYPES.includes(this.currentType)) + (isUUIDType || + isBinaryUUID || + !UNEDITABLE_TYPES.includes(this.currentType)) ); } From 0b76b6c23a5ba8268780c9bd7db282ed4db9f726 Mon Sep 17 00:00:00 2001 From: Ivan Medina Date: Tue, 10 Feb 2026 09:07:15 -0300 Subject: [PATCH 11/15] refactor: move displayType to Element property and fix table view UUID editing - Added displayType property to Element class instead of passing as constructor parameter - Removed displayType parameter from StandardEditor and UUIDEditor constructors - UUIDEditor now reads from element.displayType - Fixed table view editing for legacy UUIDs by: - Adding legacyUUIDDisplayEncoding to CellEditorProps - Setting element.displayType before creating editors in CellEditor - Updated editor() method to use displayType for editor selection - Passing legacyUUIDDisplayEncoding to cellEditorParams in document-table-view --- .../src/components/document-list/element.tsx | 7 ++- .../src/components/table-view/cell-editor.tsx | 61 ++++++++++++++++++- .../table-view/document-table-view.tsx | 1 + .../hadron-document/src/editor/standard.ts | 3 +- packages/hadron-document/src/editor/uuid.ts | 8 +-- packages/hadron-document/src/element.ts | 5 ++ 6 files changed, 74 insertions(+), 11 deletions(-) diff --git a/packages/compass-components/src/components/document-list/element.tsx b/packages/compass-components/src/components/document-list/element.tsx index 86b1727452b..d34d82f5eea 100644 --- a/packages/compass-components/src/components/document-list/element.tsx +++ b/packages/compass-components/src/components/document-list/element.tsx @@ -54,10 +54,11 @@ function useElementEditor( ) { return useMemo( () => { - // Use the display type for editor selection - this ensures that Binary UUIDs - // get the UUIDEditor even when el.currentType is 'Binary' + // Set the displayType on the element so editors can read it. + // This ensures that Binary UUIDs get the UUIDEditor even when el.currentType is 'Binary' + el.displayType = displayType; const Editor = getEditorByType(displayType); - return new Editor(el, displayType); + return new Editor(el); }, // The list of deps is exhaustive, but we want `displayType` to be an // explicit dependency of the memo to make sure that even if the `el` diff --git a/packages/compass-crud/src/components/table-view/cell-editor.tsx b/packages/compass-crud/src/components/table-view/cell-editor.tsx index 8af27bee2a9..907cfc8a214 100644 --- a/packages/compass-crud/src/components/table-view/cell-editor.tsx +++ b/packages/compass-crud/src/components/table-view/cell-editor.tsx @@ -6,6 +6,7 @@ import { ElementEditor as initEditors, getDefaultValueForType, } from 'hadron-document'; +import { Binary } from 'bson'; import TypesDropdown from './types-dropdown'; import AddFieldButton from './add-field-button'; import { @@ -59,6 +60,47 @@ export interface DocumentTableRowNode extends RowNode { }; } +/** + * Gets the display type for an element, considering legacy UUID encoding preference. + * For Binary subtype 3 (legacy UUID), returns the appropriate legacy UUID type based on encoding. + * For Binary subtype 4 (UUID), returns 'UUID'. + * For all other types, returns the element's currentType. + */ +function getDisplayType( + element: Element, + legacyUUIDEncoding?: string +): TypeCastTypes { + // If the element already has a specific UUID type, use it + if ( + element.currentType === 'UUID' || + element.currentType === 'LegacyJavaUUID' || + element.currentType === 'LegacyCSharpUUID' || + element.currentType === 'LegacyPythonUUID' + ) { + return element.currentType; + } + + // Check if this is a Binary that should be displayed as a UUID type + if ( + element.currentType === 'Binary' && + element.currentValue instanceof Binary + ) { + const binary = element.currentValue; + if (binary.sub_type === Binary.SUBTYPE_UUID) { + return 'UUID'; + } + if ( + binary.sub_type === Binary.SUBTYPE_UUID_OLD && + binary.buffer.length === 16 && + legacyUUIDEncoding + ) { + return legacyUUIDEncoding as TypeCastTypes; + } + } + + return element.currentType; +} + export type CellEditorProps = Omit & { value: Element; node: DocumentTableRowNode; @@ -75,6 +117,7 @@ export type CellEditorProps = Omit & { drillDown: CrudActions['drillDown']; tz: string; darkMode?: boolean; + legacyUUIDDisplayEncoding?: string; }; type CellEditorState = { @@ -146,6 +189,12 @@ class CellEditor this.newField = value.currentKey === '$new'; } + // Set the displayType on the element so editors can read it + this.element.displayType = getDisplayType( + this.element, + props.legacyUUIDDisplayEncoding + ); + this.oldType = this.element.currentType; this._editors = initEditors(this.element /*, props.tz*/); this.editor().start(); @@ -365,12 +414,22 @@ class CellEditor /** * Get the editor for the current type. + * Uses element.displayType if set, otherwise falls back to element.currentType. * * @returns {Editor} The editor. */ editor(): Editor { + // Use displayType if available (for UUID types), otherwise use currentType + const editorType = this.element!.displayType ?? this.element!.currentType; + // Map legacy UUID types to the UUID editor + const editorKey = + editorType === 'LegacyJavaUUID' || + editorType === 'LegacyCSharpUUID' || + editorType === 'LegacyPythonUUID' + ? 'UUID' + : editorType; return ( - this._editors![this.element!.currentType as keyof typeof this._editors] ?? + this._editors![editorKey as keyof typeof this._editors] ?? this._editors!.Standard ); } diff --git a/packages/compass-crud/src/components/table-view/document-table-view.tsx b/packages/compass-crud/src/components/table-view/document-table-view.tsx index fb01cc8e11e..e971b601a58 100644 --- a/packages/compass-crud/src/components/table-view/document-table-view.tsx +++ b/packages/compass-crud/src/components/table-view/document-table-view.tsx @@ -868,6 +868,7 @@ export class DocumentTableView extends React.Component { drillDown: this.props.drillDown, tz: this.props.tz, darkMode: this.props.darkMode, + legacyUUIDDisplayEncoding: this.props.legacyUUIDDisplayEncoding, }, resizable: true, width: this.props.columnWidths[String(path[path.length - 1])], diff --git a/packages/hadron-document/src/editor/standard.ts b/packages/hadron-document/src/editor/standard.ts index b4c04fb5050..f3a2566b948 100644 --- a/packages/hadron-document/src/editor/standard.ts +++ b/packages/hadron-document/src/editor/standard.ts @@ -22,9 +22,8 @@ export default class StandardEditor { * Create the editor with the element. * * @param element - The hadron document element. - * @param _displayType - Optional display type (used by subclasses like UUIDEditor). */ - constructor(element: Element, _displayType?: string) { + constructor(element: Element) { this.element = element; this.type = element.currentType; this.editing = false; diff --git a/packages/hadron-document/src/editor/uuid.ts b/packages/hadron-document/src/editor/uuid.ts index 0f686900265..c2295bc3546 100644 --- a/packages/hadron-document/src/editor/uuid.ts +++ b/packages/hadron-document/src/editor/uuid.ts @@ -61,13 +61,11 @@ export default class UUIDEditor extends StandardEditor { * Create the UUID editor. * * @param element - The hadron document element. - * @param displayType - Optional display type override. Used when element.currentType - * is 'Binary' but the element should be treated as a UUID type. */ - constructor(element: Element, displayType?: string) { + constructor(element: Element) { super(element); - // Use displayType if provided and it's a UUID type, otherwise fall back to element.currentType - const effectiveType = displayType ?? element.currentType; + // Use element.displayType if set and it's a UUID type, otherwise fall back to element.currentType + const effectiveType = element.displayType ?? element.currentType; this.uuidType = (UUID_TYPES as readonly string[]).includes(effectiveType) ? (effectiveType as UUIDType) : 'UUID'; diff --git a/packages/hadron-document/src/element.ts b/packages/hadron-document/src/element.ts index 7a6c7e59a89..7c5ce052a49 100644 --- a/packages/hadron-document/src/element.ts +++ b/packages/hadron-document/src/element.ts @@ -98,6 +98,11 @@ export class Element extends EventEmitter { decrypted: boolean; expanded = false; maxVisibleElementsCount = DEFAULT_VISIBLE_ELEMENTS; + // Display type for the element. This is used by editors to determine + // how to display and edit the value. For example, a Binary with subtype 3 + // might have displayType set to 'LegacyJavaUUID' to indicate it should be + // displayed and edited as a Java legacy UUID. + displayType?: TypeCastTypes; /** * Cancel any modifications to the element. From e6b65ec9ebecf6022b196eb8a48ebf38b96824c7 Mon Sep 17 00:00:00 2001 From: Ivan Medina Date: Wed, 11 Feb 2026 08:17:48 -0300 Subject: [PATCH 12/15] fix: address review comments --- .../src/components/bson-value.tsx | 12 ++++--- .../document-list/element-editors.tsx | 3 +- .../src/components/document-list/element.tsx | 34 +++++++++++-------- .../src/components/table-view/cell-editor.tsx | 9 +++-- packages/hadron-document/src/editor/uuid.ts | 32 ++++++++++------- packages/hadron-document/src/element.ts | 19 +++++++---- .../hadron-type-checker/src/type-checker.ts | 20 ++++++----- 7 files changed, 79 insertions(+), 50 deletions(-) diff --git a/packages/compass-components/src/components/bson-value.tsx b/packages/compass-components/src/components/bson-value.tsx index df1689bfd14..d8e5c2a13a4 100644 --- a/packages/compass-components/src/components/bson-value.tsx +++ b/packages/compass-components/src/components/bson-value.tsx @@ -131,19 +131,19 @@ const ObjectIdValue: React.FunctionComponent> = ({ }; const toLegacyJavaUUID = ({ value }: PropsByValueType<'Binary'>) => { - const hex = Buffer.from(value.buffer).toString('hex'); + const hex = value.toString('hex'); const reversedHex = reverseJavaUUIDBytes(hex); return "LegacyJavaUUID('" + uuidHexToString(reversedHex) + "')"; }; const toLegacyCSharpUUID = ({ value }: PropsByValueType<'Binary'>) => { - const hex = Buffer.from(value.buffer).toString('hex'); + const hex = value.toString('hex'); const reversedHex = reverseCSharpUUIDBytes(hex); return "LegacyCSharpUUID('" + uuidHexToString(reversedHex) + "')"; }; const toLegacyPythonUUID = ({ value }: PropsByValueType<'Binary'>) => { - const hex = Buffer.from(value.buffer).toString('hex'); + const hex = value.toString('hex'); return "LegacyPythonUUID('" + uuidHexToString(hex) + "')"; }; @@ -197,9 +197,13 @@ const UUIDValue: React.FunctionComponent> = ({ return String(value); } try { + // Try to get the pretty hex version of the UUID return value.toUUID().toString(); } catch { - return Buffer.from(value.buffer).toString('hex'); + // If uuid is not following the uuid format (e.g., not exactly 16 bytes), + // converting it to UUID will fail. We don't want the UI to fail rendering + // it and instead will just display the "unformatted" hex value. + return value.toString('hex'); } }, [value]); diff --git a/packages/compass-components/src/components/document-list/element-editors.tsx b/packages/compass-components/src/components/document-list/element-editors.tsx index a8a7bcba750..91ca6f9aba3 100644 --- a/packages/compass-components/src/components/document-list/element-editors.tsx +++ b/packages/compass-components/src/components/document-list/element-editors.tsx @@ -188,7 +188,8 @@ const uuidEditorLabel = css({ whiteSpace: 'nowrap', }); -function isUUIDType(type: string): boolean { +type UUIDType = (typeof UUID_TYPES)[number]; +export function isUUIDType(type: string): type is UUIDType { return (UUID_TYPES as readonly string[]).includes(type); } diff --git a/packages/compass-components/src/components/document-list/element.tsx b/packages/compass-components/src/components/document-list/element.tsx index d34d82f5eea..279c3694850 100644 --- a/packages/compass-components/src/components/document-list/element.tsx +++ b/packages/compass-components/src/components/document-list/element.tsx @@ -12,7 +12,12 @@ import { import { Binary } from 'bson'; import BSONValue from '../bson-value'; import { spacing } from '@leafygreen-ui/tokens'; -import { KeyEditor, ValueEditor, TypeEditor } from './element-editors'; +import { + KeyEditor, + ValueEditor, + TypeEditor, + isUUIDType, +} from './element-editors'; import { EditActions, AddFieldActions } from './element-actions'; import { useAutoFocusContext } from './auto-focus-context'; import { useForceUpdate } from './use-force-update'; @@ -38,12 +43,10 @@ function getEditorByType(type: HadronElementType['type']) { case 'Undefined': case 'ObjectId': return ElementEditor[`${type}Editor` as const]; - case 'UUID': - case 'LegacyJavaUUID': - case 'LegacyCSharpUUID': - case 'LegacyPythonUUID': - return ElementEditor.UUIDEditor; default: + if (isUUIDType(type)) { + return ElementEditor.UUIDEditor; + } return ElementEditor.StandardEditor; } } @@ -80,18 +83,21 @@ function getDisplayType( legacyUUIDEncoding: string ): HadronElementType['type'] { // If the element already has a specific UUID type, use it - if ( - el.currentType === 'UUID' || - el.currentType === 'LegacyJavaUUID' || - el.currentType === 'LegacyCSharpUUID' || - el.currentType === 'LegacyPythonUUID' - ) { + if (isUUIDType(el.currentType)) { return el.currentType; } // Check if this is a Binary that should be displayed as a UUID type - if (el.currentType === 'Binary' && el.currentValue instanceof Binary) { - const binary = el.currentValue; + // Using _bsontype check instead of instanceof for cross-realm compatibility + // and future bson@7.x compatibility + if ( + el.currentType === 'Binary' && + el.currentValue && + typeof el.currentValue === 'object' && + '_bsontype' in el.currentValue && + el.currentValue._bsontype === 'Binary' + ) { + const binary = el.currentValue as Binary; if (binary.sub_type === Binary.SUBTYPE_UUID) { return 'UUID'; } diff --git a/packages/compass-crud/src/components/table-view/cell-editor.tsx b/packages/compass-crud/src/components/table-view/cell-editor.tsx index 907cfc8a214..60ad1a357e8 100644 --- a/packages/compass-crud/src/components/table-view/cell-editor.tsx +++ b/packages/compass-crud/src/components/table-view/cell-editor.tsx @@ -81,11 +81,16 @@ function getDisplayType( } // Check if this is a Binary that should be displayed as a UUID type + // Using _bsontype check instead of instanceof for cross-realm compatibility + // and future bson@7.x compatibility if ( element.currentType === 'Binary' && - element.currentValue instanceof Binary + element.currentValue && + typeof element.currentValue === 'object' && + '_bsontype' in element.currentValue && + element.currentValue._bsontype === 'Binary' ) { - const binary = element.currentValue; + const binary = element.currentValue as Binary; if (binary.sub_type === Binary.SUBTYPE_UUID) { return 'UUID'; } diff --git a/packages/hadron-document/src/editor/uuid.ts b/packages/hadron-document/src/editor/uuid.ts index c2295bc3546..4dbe69802fb 100644 --- a/packages/hadron-document/src/editor/uuid.ts +++ b/packages/hadron-document/src/editor/uuid.ts @@ -7,7 +7,7 @@ import { Binary } from 'bson'; import { ElementEvents } from '../element-events'; import StandardEditor from './standard'; import type { Element } from '../element'; -import { UUID_TYPES, type UUIDType } from '../element'; +import { isUUIDType, type UUIDType } from '../element'; import type { BSONValue } from '../utils'; /** @@ -18,7 +18,7 @@ const binaryToUUIDString = (binary: Binary): string => { return binary.toUUID().toString(); } catch { // Fallback to hex if toUUID fails - return Buffer.from(binary.buffer).toString('hex'); + return binary.toString('hex'); } }; @@ -27,7 +27,7 @@ const binaryToUUIDString = (binary: Binary): string => { * Java legacy format reverses byte order for both MSB and LSB. */ const binaryToLegacyJavaUUIDString = (binary: Binary): string => { - const hex = Buffer.from(binary.buffer).toString('hex'); + const hex = binary.toString('hex'); const reversedHex = reverseJavaUUIDBytes(hex); return uuidHexToString(reversedHex); }; @@ -37,7 +37,7 @@ const binaryToLegacyJavaUUIDString = (binary: Binary): string => { * C# legacy format reverses byte order for first 3 groups only. */ const binaryToLegacyCSharpUUIDString = (binary: Binary): string => { - const hex = Buffer.from(binary.buffer).toString('hex'); + const hex = binary.toString('hex'); const reversedHex = reverseCSharpUUIDBytes(hex); return uuidHexToString(reversedHex); }; @@ -47,7 +47,7 @@ const binaryToLegacyCSharpUUIDString = (binary: Binary): string => { * Python legacy format uses direct byte order (no reversal). */ const binaryToLegacyPythonUUIDString = (binary: Binary): string => { - const hex = Buffer.from(binary.buffer).toString('hex'); + const hex = binary.toString('hex'); return uuidHexToString(hex); }; @@ -66,9 +66,7 @@ export default class UUIDEditor extends StandardEditor { super(element); // Use element.displayType if set and it's a UUID type, otherwise fall back to element.currentType const effectiveType = element.displayType ?? element.currentType; - this.uuidType = (UUID_TYPES as readonly string[]).includes(effectiveType) - ? (effectiveType as UUIDType) - : 'UUID'; + this.uuidType = isUUIDType(effectiveType) ? effectiveType : 'UUID'; } /** @@ -81,17 +79,25 @@ export default class UUIDEditor extends StandardEditor { return val; } // If it's a Binary, convert to UUID string based on the type - if (val instanceof Binary) { + // Using _bsontype check instead of instanceof for cross-realm compatibility + // and future bson@7.x compatibility + if ( + val && + typeof val === 'object' && + '_bsontype' in val && + val._bsontype === 'Binary' + ) { + const binary = val as Binary; switch (this.uuidType) { case 'LegacyJavaUUID': - return binaryToLegacyJavaUUIDString(val); + return binaryToLegacyJavaUUIDString(binary); case 'LegacyCSharpUUID': - return binaryToLegacyCSharpUUIDString(val); + return binaryToLegacyCSharpUUIDString(binary); case 'LegacyPythonUUID': - return binaryToLegacyPythonUUIDString(val); + return binaryToLegacyPythonUUIDString(binary); case 'UUID': default: - return binaryToUUIDString(val); + return binaryToUUIDString(binary); } } return String(val); diff --git a/packages/hadron-document/src/element.ts b/packages/hadron-document/src/element.ts index 7c5ce052a49..187608ec4db 100644 --- a/packages/hadron-document/src/element.ts +++ b/packages/hadron-document/src/element.ts @@ -64,6 +64,13 @@ export const UUID_TYPES = [ export type UUIDType = (typeof UUID_TYPES)[number]; +/** + * Type guard to check if a type string is a UUID type. + */ +export function isUUIDType(type: string): type is UUIDType { + return (UUID_TYPES as readonly string[]).includes(type); +} + export const DEFAULT_VISIBLE_ELEMENTS = 25; export function isValueExpandable( value: BSONValue @@ -263,8 +270,8 @@ export class Element extends EventEmitter { editor.edit(this.generateObject()); editor.complete(); } else if ( - (UUID_TYPES as readonly string[]).includes(newType) && - (UUID_TYPES as readonly string[]).includes(this.currentType) && + isUUIDType(newType) && + isUUIDType(this.currentType) && this.currentValue instanceof Binary ) { // Special handling for converting between UUID types @@ -281,7 +288,7 @@ export class Element extends EventEmitter { this.edit(TypeChecker.cast(this.generateObject(), newType)); // For UUID types, explicitly set the currentType since TypeChecker.type() // may not return the specific UUID type for legacy UUIDs (subtype 3) - if ((UUID_TYPES as readonly string[]).includes(newType)) { + if (isUUIDType(newType)) { this.currentType = newType; // Fire another event to notify the UI of the type change this._bubbleUp(ElementEvents.Edited, this); @@ -691,9 +698,7 @@ export class Element extends EventEmitter { */ isValueEditable(): boolean { // UUID types are editable even though Binary is in UNEDITABLE_TYPES - const isUUIDType = (UUID_TYPES as readonly string[]).includes( - this.currentType - ); + const isCurrentTypeUUID = isUUIDType(this.currentType); // Also check for Binary values that are actually UUIDs (subtype 3 or 4) // This handles the case where currentType is 'Binary' but it's actually a UUID const isBinaryUUID = @@ -704,7 +709,7 @@ export class Element extends EventEmitter { this.currentValue.buffer.length === 16)); return ( this._isKeyLegallyEditable() && - (isUUIDType || + (isCurrentTypeUUID || isBinaryUUID || !UNEDITABLE_TYPES.includes(this.currentType)) ); diff --git a/packages/hadron-type-checker/src/type-checker.ts b/packages/hadron-type-checker/src/type-checker.ts index 317ec725ecb..435c1579a00 100644 --- a/packages/hadron-type-checker/src/type-checker.ts +++ b/packages/hadron-type-checker/src/type-checker.ts @@ -303,6 +303,11 @@ export const uuidHexToString = (hex: string): string => { /** * 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) */ export const reverseJavaUUIDBytes = (hex: string): string => { let msb = hex.substring(0, 16); @@ -331,6 +336,11 @@ export const reverseJavaUUIDBytes = (hex: string): string => { /** * 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) */ export const reverseCSharpUUIDBytes = (hex: string): string => { const a = @@ -351,14 +361,6 @@ const generateRandomUUID = (): string => { return new UUID().toString(); }; -/** - * Extracts the raw hex string from a Binary UUID (subtype 3 or 4). - * Returns the hex without any byte order reversal. - */ -const binaryToRawHex = (binary: Binary): string => { - return Buffer.from(binary.buffer).toString('hex'); -}; - /** * 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. @@ -369,7 +371,7 @@ const binaryToUUIDStringWithEncoding = ( binary: Binary, sourceEncoding?: 'Java' | 'CSharp' | 'Python' ): string => { - const hex = binaryToRawHex(binary); + const hex = binary.toString('hex'); // For standard UUID (subtype 4), no byte reversal needed if (binary.sub_type === Binary.SUBTYPE_UUID) { From 2e04fef6bf5ed30936ab85666642ddf384ef7a79 Mon Sep 17 00:00:00 2001 From: Ivan Medina Date: Wed, 11 Feb 2026 10:10:48 -0300 Subject: [PATCH 13/15] fix: address code review feedback - Add type predicate to isUUIDType function for better type narrowing - Export and reuse isUUIDType function across packages - Replace instanceof Binary checks with _bsontype property checks for cross-realm compatibility - Add explanatory comment for catch block in bson-value.tsx - Replace Buffer.from(binary.buffer).toString('hex') with binary.toString('hex') - Add isUUIDType function to hadron-document package - Clarify byte reversal function documentation (involutions) - Remove unnecessary binaryToRawHex helper function - Fix ESLint errors: use import type for Binary, remove unnecessary type assertion --- packages/hadron-document/src/editor/uuid.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/hadron-document/src/editor/uuid.ts b/packages/hadron-document/src/editor/uuid.ts index 4dbe69802fb..a2a64b597ec 100644 --- a/packages/hadron-document/src/editor/uuid.ts +++ b/packages/hadron-document/src/editor/uuid.ts @@ -3,7 +3,7 @@ import TypeChecker, { reverseJavaUUIDBytes, reverseCSharpUUIDBytes, } from 'hadron-type-checker'; -import { Binary } from 'bson'; +import type { Binary } from 'bson'; import { ElementEvents } from '../element-events'; import StandardEditor from './standard'; import type { Element } from '../element'; @@ -87,7 +87,7 @@ export default class UUIDEditor extends StandardEditor { '_bsontype' in val && val._bsontype === 'Binary' ) { - const binary = val as Binary; + const binary = val; switch (this.uuidType) { case 'LegacyJavaUUID': return binaryToLegacyJavaUUIDString(binary); From 0f4f9e1506069e37a205f4bf482ced9a7eac2d82 Mon Sep 17 00:00:00 2001 From: Ivan Medina Date: Wed, 11 Feb 2026 10:15:02 -0300 Subject: [PATCH 14/15] fix: replace remaining instanceof Binary checks with _bsontype property checks - Add isBinary() helper function with TODO for bson@7.x upgrade - Replace instanceof Binary checks in element.ts with isBinary() helper - Uses _bsontype property check for cross-realm compatibility --- packages/hadron-document/src/element.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/hadron-document/src/element.ts b/packages/hadron-document/src/element.ts index 187608ec4db..11c52b7475f 100644 --- a/packages/hadron-document/src/element.ts +++ b/packages/hadron-document/src/element.ts @@ -71,6 +71,20 @@ export function isUUIDType(type: string): type is UUIDType { return (UUID_TYPES as readonly string[]).includes(type); } +/** + * Type guard to check if a value is a BSON Binary. + * Uses _bsontype property check instead of instanceof for cross-realm compatibility. + * TODO: Once we upgrade to bson@7.x, use value?.[bsonType] === 'Binary' instead. + */ +function isBinary(value: unknown): value is Binary { + return ( + value !== null && + typeof value === 'object' && + '_bsontype' in value && + value._bsontype === 'Binary' + ); +} + export const DEFAULT_VISIBLE_ELEMENTS = 25; export function isValueExpandable( value: BSONValue @@ -272,7 +286,7 @@ export class Element extends EventEmitter { } else if ( isUUIDType(newType) && isUUIDType(this.currentType) && - this.currentValue instanceof Binary + isBinary(this.currentValue) ) { // Special handling for converting between UUID types // We need to use the source type to properly decode the binary @@ -701,16 +715,16 @@ export class Element extends EventEmitter { const isCurrentTypeUUID = isUUIDType(this.currentType); // Also check for Binary values that are actually UUIDs (subtype 3 or 4) // This handles the case where currentType is 'Binary' but it's actually a UUID - const isBinaryUUID = + const isBinaryUUIDValue = this.currentType === 'Binary' && - this.currentValue instanceof Binary && + isBinary(this.currentValue) && (this.currentValue.sub_type === Binary.SUBTYPE_UUID || (this.currentValue.sub_type === Binary.SUBTYPE_UUID_OLD && this.currentValue.buffer.length === 16)); return ( this._isKeyLegallyEditable() && (isCurrentTypeUUID || - isBinaryUUID || + isBinaryUUIDValue || !UNEDITABLE_TYPES.includes(this.currentType)) ); } From f0a07bec710761244d0aa7f967caabfb4ca63291 Mon Sep 17 00:00:00 2001 From: Ivan Medina Date: Wed, 11 Feb 2026 10:56:02 -0300 Subject: [PATCH 15/15] trigger ci