Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions packages/compass-components/src/components/bson-value.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import BSONValue from './bson-value';
import { expect } from 'chai';
import { render, cleanup, screen } from '@mongodb-js/testing-library-compass';
import { LegacyUUIDDisplayContext } from './document-list/legacy-uuid-format-context';

describe('BSONValue', function () {
afterEach(cleanup);
Expand Down Expand Up @@ -45,6 +46,11 @@ describe('BSONValue', function () {
value: Binary.createFromHexString('3132303d', Binary.SUBTYPE_UUID),
expected: "UUID('3132303d')",
},
{
type: 'Binary',
value: Binary.createFromBase64('dGVzdA==', Binary.SUBTYPE_UUID_OLD),
expected: "Binary.createFromBase64('dGVzdA==', 3)",
},
{
type: 'Binary',
value: Binary.fromInt8Array(new Int8Array([1, 2, 3])),
Expand Down Expand Up @@ -159,4 +165,109 @@ describe('BSONValue', function () {
expect(await screen.findByTestId('bson-value-in-use-encryption-docs-link'))
.to.be.visible;
});

describe('Legacy UUID display formats', function () {
const legacyUuidBinary = Binary.createFromHexString(
'0123456789abcdef0123456789abcdef',
Binary.SUBTYPE_UUID_OLD
);

it('should render Legacy UUID without encoding (raw format)', function () {
const { container } = render(
<LegacyUUIDDisplayContext.Provider value="">
<BSONValue type="Binary" value={legacyUuidBinary} />
</LegacyUUIDDisplayContext.Provider>
);

expect(container.querySelector('.element-value')?.textContent).to.include(
"Binary.createFromBase64('ASNFZ4mrze8BI0VniavN7w==', 3)"
);
});

it('should render Legacy UUID in Java format', function () {
const { container } = render(
<LegacyUUIDDisplayContext.Provider value="LegacyJavaUUID">
<BSONValue type="Binary" value={legacyUuidBinary} />
</LegacyUUIDDisplayContext.Provider>
);

expect(container.querySelector('.element-value')?.textContent).to.eq(
'LegacyJavaUUID("efcdab89-6745-2301-efcd-ab8967452301")'
);
});

it('should render Legacy UUID in C# format', function () {
const { container } = render(
<LegacyUUIDDisplayContext.Provider value="LegacyCSharpUUID">
<BSONValue type="Binary" value={legacyUuidBinary} />
</LegacyUUIDDisplayContext.Provider>
);

expect(container.querySelector('.element-value')?.textContent).to.eq(
'LegacyCSharpUUID("67452301-ab89-efcd-0123-456789abcdef")'
);
});

it('should render Legacy UUID in Python format', function () {
const { container } = render(
<LegacyUUIDDisplayContext.Provider value="LegacyPythonUUID">
<BSONValue type="Binary" value={legacyUuidBinary} />
</LegacyUUIDDisplayContext.Provider>
);

expect(container.querySelector('.element-value')?.textContent).to.eq(
'LegacyPythonUUID("01234567-89ab-cdef-0123-456789abcdef")'
);
});

it('should fallback to raw format if UUID conversion fails', function () {
// Create an invalid UUID binary that will cause conversion to fail.
const invalidUuidBinary = new Binary(
Buffer.from('invalid'),
Binary.SUBTYPE_UUID_OLD
);

const { container } = render(
<LegacyUUIDDisplayContext.Provider value="LegacyJavaUUID">
<BSONValue type="Binary" value={invalidUuidBinary} />
</LegacyUUIDDisplayContext.Provider>
);

expect(container.querySelector('.element-value')?.textContent).to.include(
'Binary.createFromBase64('
);
});

it('should fallback to raw format for all Legacy UUID formats on error', function () {
const invalidUuidBinary = new Binary(
Buffer.from('invalid'),
Binary.SUBTYPE_UUID_OLD
);

const formats = [
'LegacyJavaUUID',
'LegacyCSharpUUID',
'LegacyPythonUUID',
] as const;

formats.forEach((format) => {
const { container } = render(
<LegacyUUIDDisplayContext.Provider value={format}>
<BSONValue type="Binary" value={invalidUuidBinary} />
</LegacyUUIDDisplayContext.Provider>
);

expect(
container.querySelector('.element-value')?.textContent
).to.include(
'Binary.createFromBase64(',
`${format} should fallback to raw format`
);
expect(
container.querySelector('.element-value')?.textContent
).to.include(', 3)', `${format} should show subtype 3`);
cleanup();
});
});
});
});
127 changes: 126 additions & 1 deletion packages/compass-components/src/components/bson-value.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Icon, Link } from './leafygreen';
import { spacing } from '@leafygreen-ui/tokens';
import { css, cx } from '@leafygreen-ui/emotion';
import { Theme, useDarkMode } from '../hooks/use-theme';
import { useLegacyUUIDDisplayContext } from './document-list/legacy-uuid-format-context';

type ValueProps =
| {
Expand Down Expand Up @@ -123,6 +124,125 @@ const ObjectIdValue: React.FunctionComponent<PropsByValueType<'ObjectId'>> = ({
);
};

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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/mongodb-js/mongosh/blob/adc86b57327c2f4556fe888692826a86ab7f5293/packages/shell-bson/src/shell-bson.ts#L348

Is it not possible to reuse this package and it's logic for displaying and reordering bytes?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, I didn't end up reusing @mongosh/shell-bson or @mongodb-js/shell-bson-parser, at least yet, as they're both used for parsing a string, and here we're displaying the Binary data in the format with the dashes. There is still some duplication in the re-ordering of bytes. We will soon to have the displaying logic in mongodb-query-parser that we have added here so I'm thinking that'll be a good time to do some reusing.
That'll be once we update how we display Binary subtype 3 when we want to show it in shell syntax.

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 reversed = msb + lsb;
const uuid =
reversed.substring(0, 8) +
'-' +
reversed.substring(8, 12) +
'-' +
reversed.substring(12, 16) +
'-' +
reversed.substring(16, 20) +
'-' +
reversed.substring(20, 32);
return 'LegacyJavaUUID("' + uuid + '")';
};

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 reversed = a + b + c + d;
const uuid =
reversed.substring(0, 8) +
'-' +
reversed.substring(8, 12) +
'-' +
reversed.substring(12, 16) +
'-' +
reversed.substring(16, 20) +
'-' +
reversed.substring(20, 32);
return 'LegacyCSharpUUID("' + uuid + '")';
};

const toLegacyPythonUUID = ({ value }: PropsByValueType<'Binary'>) => {
// Get the hex representation from the buffer.
const hex = Buffer.from(value.buffer).toString('hex');
// Python format uses the hex like UUID in subtype 4.
const uuid =
hex.substring(0, 8) +
'-' +
hex.substring(8, 12) +
'-' +
hex.substring(12, 16) +
'-' +
hex.substring(16, 20) +
'-' +
hex.substring(20, 32);
return 'LegacyPythonUUID("' + uuid + '")';
};

// Binary sub_type 3.
const LegacyUUIDValue: React.FunctionComponent<PropsByValueType<'Binary'>> = (
bsonValue
) => {
const legacyUUIDDisplayEncoding = useLegacyUUIDDisplayContext();

const stringifiedValue = useMemo(() => {
// UUID must be exactly 16 bytes.
if (bsonValue.value.buffer.length === 16) {
try {
if (legacyUUIDDisplayEncoding === 'LegacyJavaUUID') {
return toLegacyJavaUUID(bsonValue);
} else if (legacyUUIDDisplayEncoding === 'LegacyCSharpUUID') {
return toLegacyCSharpUUID(bsonValue);
} else if (legacyUUIDDisplayEncoding === 'LegacyPythonUUID') {
return toLegacyPythonUUID(bsonValue);
}
} catch {
// Ignore errors and fallback to the raw representation.
// The UUID conversion can fail if the binary data is not a valid UUID.
}
}

// Raw, no encoding.
return `Binary.createFromBase64('${truncate(
bsonValue.value.toString('base64'),
100
)}', ${bsonValue.value.sub_type})`;
}, [legacyUUIDDisplayEncoding, bsonValue]);

return (
<BSONValueContainer type="Binary" title={stringifiedValue}>
{stringifiedValue}
</BSONValueContainer>
);
};

const BinaryValue: React.FunctionComponent<PropsByValueType<'Binary'>> = ({
value,
}) => {
Expand Down Expand Up @@ -241,7 +361,9 @@ const DateValue: React.FunctionComponent<PropsByValueType<'Date'>> = ({
};

const NumberValue: React.FunctionComponent<
PropsByValueType<'Int32' | 'Double'> & { type: 'Int32' | 'Double' }
PropsByValueType<'Int32' | 'Double' | 'Int64' | 'Decimal128'> & {
type: 'Int32' | 'Double' | 'Int64' | 'Decimal128';
}
> = ({ type, value }) => {
const stringifiedValue = useMemo(() => {
return String(value.valueOf());
Expand Down Expand Up @@ -376,6 +498,9 @@ const BSONValue: React.FunctionComponent<ValueProps> = (props) => {
case 'Date':
return <DateValue value={props.value}></DateValue>;
case 'Binary':
if (props.value.sub_type === Binary.SUBTYPE_UUID_OLD) {
return <LegacyUUIDValue value={props.value}></LegacyUUIDValue>;
}
return <BinaryValue value={props.value}></BinaryValue>;
case 'Int32':
case 'Double':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import {
} from './context-menu';
import { DrawerContentProvider } from './drawer-portal';
import { CopyPasteContextMenu } from '../hooks/use-copy-paste-context-menu';
import {
type LegacyUUIDDisplay,
LegacyUUIDDisplayContext,
} from './document-list/legacy-uuid-format-context';

type GuideCueProviderProps = React.ComponentProps<typeof GuideCueProvider>;

Expand All @@ -22,6 +26,7 @@ type CompassComponentsProviderProps = {
* value will be derived from the system settings
*/
darkMode?: boolean;
legacyUUIDDisplayEncoding?: LegacyUUIDDisplay;
popoverPortalContainer?: HTMLElement;
/**
* Either React children or a render callback that will get the darkMode
Expand Down Expand Up @@ -124,6 +129,7 @@ function useDarkMode(_darkMode?: boolean) {
export const CompassComponentsProvider = ({
darkMode: _darkMode,
children,
legacyUUIDDisplayEncoding,
onNextGuideGue,
onNextGuideCueGroup,
onContextMenuOpen,
Expand Down Expand Up @@ -161,45 +167,49 @@ export const CompassComponentsProvider = ({
darkMode={darkMode}
popoverPortalContainer={popoverPortalContainer}
>
<DrawerContentProvider
onDrawerSectionOpen={onDrawerSectionOpen}
onDrawerSectionHide={onDrawerSectionHide}
<LegacyUUIDDisplayContext.Provider
value={legacyUUIDDisplayEncoding ?? ''}
>
<StackedComponentProvider zIndex={stackedElementsZIndex}>
<RequiredURLSearchParamsProvider
utmSource={utmSource}
utmMedium={utmMedium}
>
<GuideCueProvider
onNext={onNextGuideGue}
onNextGroup={onNextGuideCueGroup}
disabled={disableGuideCues}
<DrawerContentProvider
onDrawerSectionOpen={onDrawerSectionOpen}
onDrawerSectionHide={onDrawerSectionHide}
>
<StackedComponentProvider zIndex={stackedElementsZIndex}>
<RequiredURLSearchParamsProvider
utmSource={utmSource}
utmMedium={utmMedium}
>
<SignalHooksProvider {...signalHooksProviderProps}>
<ConfirmationModalArea>
<ContextMenuProvider
disabled={disableContextMenus}
onContextMenuOpen={onContextMenuOpen}
onContextMenuItemClick={onContextMenuItemClick}
>
<CopyPasteContextMenu>
<ToastArea>
{typeof children === 'function'
? children({
darkMode,
portalContainerRef: setPortalContainer,
scrollContainerRef: setScrollContainer,
})
: children}
</ToastArea>
</CopyPasteContextMenu>
</ContextMenuProvider>
</ConfirmationModalArea>
</SignalHooksProvider>
</GuideCueProvider>
</RequiredURLSearchParamsProvider>
</StackedComponentProvider>
</DrawerContentProvider>
<GuideCueProvider
onNext={onNextGuideGue}
onNextGroup={onNextGuideCueGroup}
disabled={disableGuideCues}
>
<SignalHooksProvider {...signalHooksProviderProps}>
<ConfirmationModalArea>
<ContextMenuProvider
disabled={disableContextMenus}
onContextMenuOpen={onContextMenuOpen}
onContextMenuItemClick={onContextMenuItemClick}
>
<CopyPasteContextMenu>
<ToastArea>
{typeof children === 'function'
? children({
darkMode,
portalContainerRef: setPortalContainer,
scrollContainerRef: setScrollContainer,
})
: children}
</ToastArea>
</CopyPasteContextMenu>
</ContextMenuProvider>
</ConfirmationModalArea>
</SignalHooksProvider>
</GuideCueProvider>
</RequiredURLSearchParamsProvider>
</StackedComponentProvider>
</DrawerContentProvider>
</LegacyUUIDDisplayContext.Provider>
</LeafyGreenProvider>
);
};
Loading