Skip to content

Commit e813746

Browse files
authored
fix: correctly format several values in browser-repl (#395)
Update the formatting for several values in the browser REPL. Since fixing this for Date objects consistently enabled doing straightforward fixes for a few other issues, this addresses: - COMPASS-4440 (Consistently formatting `Date` objects) - COMPASS-4441 (Ignoring `undefined` results) - MONGOSH-410 (`sh.status()` formatting) - COMPASS-4465 (`ObjectId` formatting in nested objects) Also include a minor change to the evergreen config that may or may not have resolved a surprising failure when this PR was first opened.
1 parent 454bd1b commit e813746

File tree

14 files changed

+288
-96
lines changed

14 files changed

+288
-96
lines changed

.evergreen/.install_node

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
set -e
12
export NODE_JS_VERSION='12.18.4'
3+
export NVM_DIR="$HOME/.nvm"
24

35
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash
4-
export NVM_DIR="$HOME/.nvm"
56

67
echo "Setting NVM environment home: $NVM_DIR"
78
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

.evergreen/.setup_env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
set -e
12
export NVM_DIR="$HOME/.nvm"
23
echo "Setting NVM environment home: $NVM_DIR"
34
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"

packages/browser-repl/src/components/shell-output-line.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ describe('<ShellOutputLine />', () => {
149149
}
150150
}} />);
151151

152-
expect(wrapper.text()).to.include('---');
152+
expect(wrapper.find('hr')).to.have.lengthOf(1);
153153
expect(wrapper.text()).to.include('metadata');
154154
});
155155

packages/browser-repl/src/components/shell-output-line.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ import { ShowCollectionsOutput } from './types/show-collections-output';
1313
import { CursorOutput } from './types/cursor-output';
1414
import { CursorIterationResultOutput } from './types/cursor-iteration-result-output';
1515
import { ObjectOutput } from './types/object-output';
16+
import { StatsResultOutput } from './types/stats-result-output';
1617
import { SimpleTypeOutput } from './types/simple-type-output';
1718
import { ErrorOutput } from './types/error-output';
18-
import { inspect } from './utils/inspect';
1919
import { ShowProfileOutput } from './types/show-profile-output';
2020

2121
const styles = require('./shell-output-line.less');
@@ -47,6 +47,10 @@ export class ShellOutputLine extends Component<ShellOutputLineProps> {
4747
return <pre>{value}</pre>;
4848
}
4949

50+
if (typeof value === 'string' && type !== null) {
51+
return <SimpleTypeOutput value={value} raw />;
52+
}
53+
5054
if (this.isPrimitiveOrFunction(value)) {
5155
return <SimpleTypeOutput value={value} />;
5256
}
@@ -60,10 +64,7 @@ export class ShellOutputLine extends Component<ShellOutputLineProps> {
6064
}
6165

6266
if (type === 'StatsResult') {
63-
const res = Object.keys(value).map(c => {
64-
return `${c}\n${inspect(value[c])}`;
65-
}).join('\n---\n');
66-
return <SimpleTypeOutput value={res} />;
67+
return <StatsResultOutput value={value} />;
6768
}
6869

6970
if (type === 'ListCommandsResult)') {

packages/browser-repl/src/components/shell-output.spec.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ describe('<ShellOutput />', () => {
1717
expect(wrapper.find(ShellOutputLine)).to.have.lengthOf(1);
1818
});
1919

20+
it('renders no output lines if only one with a value of undefined is passed', () => {
21+
const line1: ShellOutputEntry = { type: 'output', value: undefined };
22+
const wrapper = shallow(<ShellOutput output={[line1]} />);
23+
expect(wrapper.find(ShellOutputLine)).to.have.lengthOf(0);
24+
});
25+
2026
it('pass the entry to the output line as prop', () => {
2127
const line1: ShellOutputEntry = { type: 'output', value: 'line 1' };
2228
const wrapper = shallow(<ShellOutput output={[line1]} />);

packages/browser-repl/src/components/shell-output.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ export class ShellOutput extends Component<ShellOutputProps> {
1818
};
1919

2020
render(): JSX.Element[] {
21-
return this.props.output.map(this.renderLine);
21+
return this.props.output.filter(entry => entry.value !== undefined).map(this.renderLine);
2222
}
2323
}

packages/browser-repl/src/components/types/simple-type-output.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,18 @@ import { inspect } from '../utils/inspect';
55

66
interface SimpleTypeOutputProps {
77
value: any;
8+
raw?: boolean;
89
}
910

1011
export class SimpleTypeOutput extends Component<SimpleTypeOutputProps> {
1112
static propTypes = {
12-
value: PropTypes.any
13+
value: PropTypes.any,
14+
raw: PropTypes.bool
1315
};
1416

1517
render(): JSX.Element {
16-
return (<SyntaxHighlight code={inspect(this.props.value)} />);
18+
const asString = this.props.raw ? this.props.value : inspect(this.props.value);
19+
return (<SyntaxHighlight code={asString} />);
1720
}
1821
}
1922

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React, { Component } from 'react';
2+
import PropTypes from 'prop-types';
3+
import { ObjectOutput } from './object-output';
4+
5+
interface StatsResultOutputProps {
6+
value: Record<string, any>;
7+
}
8+
9+
export class StatsResultOutput extends Component<StatsResultOutputProps> {
10+
static propTypes = {
11+
value: PropTypes.any
12+
};
13+
14+
render(): JSX.Element {
15+
const result: JSX.Element[] = [];
16+
for (const [ key, value ] of Object.entries(this.props.value)) {
17+
if (result.length > 0) {
18+
result.push(<hr key={`${key}-separator`} />);
19+
}
20+
result.push(<div key={key}>
21+
<h4>{key}</h4>
22+
<ObjectOutput value={value} />
23+
</div>);
24+
}
25+
return <div>{result}</div>;
26+
}
27+
}

packages/browser-repl/src/components/utils/inspect.spec.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,46 @@ describe('inspect', () => {
3737
inspect(undefined)
3838
).to.equal('undefined');
3939
});
40+
41+
it('inspects Dates', () => {
42+
expect(
43+
inspect(new Date('2020-11-06T14:26:29.131Z'))
44+
).to.equal('2020-11-06T14:26:29.131Z');
45+
});
4046
});
4147

4248
context('with BSON types', () => {
4349
it('inspects ObjectId', () => {
4450
expect(
4551
inspect(new bson.ObjectId('0000007b3db627730e26fd0b'))
46-
).to.equal('ObjectID("0000007b3db627730e26fd0b")');
52+
).to.equal('ObjectId("0000007b3db627730e26fd0b")');
53+
});
54+
55+
it('inspects UUID', () => {
56+
expect(
57+
inspect(new bson.Binary('abcdefghiklmnopq', 4))
58+
).to.equal('UUID("61626364-6566-6768-696b-6c6d6e6f7071")');
59+
});
60+
61+
it('inspects nested ObjectId', () => {
62+
expect(
63+
inspect({ p: new bson.ObjectId('0000007b3db627730e26fd0b') })
64+
).to.equal('{ p: ObjectId("0000007b3db627730e26fd0b") }');
65+
});
66+
67+
it('inspects nested UUID', () => {
68+
expect(
69+
inspect({ p: new bson.Binary('abcdefghiklmnopq', 4) })
70+
).to.equal('{ p: UUID("61626364-6566-6768-696b-6c6d6e6f7071") }');
71+
});
72+
73+
it('does not require BSON types to be instances of the current bson library', () => {
74+
expect(
75+
inspect({
76+
_bsontype: 'ObjectID',
77+
toHexString() { return '0000007b3db627730e26fd0b'; }
78+
})
79+
).to.equal('ObjectId("0000007b3db627730e26fd0b")');
4780
});
4881
});
4982

@@ -56,4 +89,13 @@ describe('inspect', () => {
5689
});
5790
});
5891
});
92+
93+
context('with frozen objects with _bsontype properties', () => {
94+
expect(
95+
() => inspect(Object.freeze({
96+
_bsontype: 'ObjectID',
97+
toHexString() { return '0000007b3db627730e26fd0b'; }
98+
}))
99+
).not.to.throw;
100+
});
59101
});

packages/browser-repl/src/components/utils/inspect.ts

Lines changed: 65 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,85 @@
11
import { inspect as utilInspect } from 'util';
2-
type BSONBaseType = { _bsontype: string };
2+
import { bsonStringifiers } from '@mongosh/service-provider-core';
33

4-
const formatBsonType = (value: BSONBaseType): any => ({
5-
inspect(): string {
6-
return `${value._bsontype}(${(JSON.stringify(value))})`;
7-
}
8-
});
4+
// At the time of writing, the Compass dist package contains what appear to be
5+
// 155 different copies of the 'bson' module. It is impractical to attach
6+
// our inspection methods to each of those copies individually, like we do when
7+
// we are inside cli-repl.
8+
// Instead, we look for values with a _bsontype property inside the object graph
9+
// before printing them here, and attach inspection methods to each of them
10+
// individually.
11+
// This is not particularly fast, but should work just fine for user-facing
12+
// interfaces like printing shell output in the browser.
913

10-
function isBsonType(value: any): boolean {
11-
return !!(value && value._bsontype);
12-
}
14+
const customInspect = utilInspect.custom || 'inspect';
15+
const visitedObjects = new WeakSet();
1316

14-
function isObject(value: any): boolean {
15-
return !!(value && typeof value === 'object' && !Array.isArray(value));
17+
function tryAddInspect(obj: any, stringifier: (this: any) => string): void {
18+
try {
19+
Object.defineProperty(obj, customInspect, {
20+
writable: true,
21+
configurable: true,
22+
enumerable: false,
23+
value: function() {
24+
try {
25+
return stringifier.call(this);
26+
} catch (err) {
27+
console.warn('Could not inspect bson object', { obj: this, err });
28+
return utilInspect(this, { customInspect: false });
29+
}
30+
}
31+
});
32+
} catch (err) {
33+
console.warn('Could not add inspect key to object', { obj, err });
34+
}
1635
}
1736

18-
function formatProperty(value: any): any {
19-
if (isObject(value) && isBsonType(value)) {
20-
return formatBsonType(value);
37+
function isDate(value: any): boolean {
38+
try {
39+
Date.prototype.getTime.call(value);
40+
return true;
41+
} catch {
42+
return false;
2143
}
22-
23-
return value;
2444
}
2545

26-
function formatObject(object: any): any {
27-
const viewObject: any = {};
28-
for (const key of Object.keys(object)) {
29-
viewObject[key] = formatProperty(object[key]);
46+
function attachInspectMethods(obj: any): void {
47+
if ((typeof obj !== 'object' && typeof obj !== 'function') || obj === null) {
48+
// Ignore primitives
49+
return;
3050
}
31-
return viewObject;
32-
}
3351

34-
function toViewValue(value: any): any {
35-
if (isBsonType(value)) {
36-
return formatBsonType(value);
52+
if (visitedObjects.has(obj)) {
53+
return;
54+
}
55+
visitedObjects.add(obj);
56+
57+
// Traverse the rest of the object graph.
58+
attachInspectMethods(Object.getPrototypeOf(obj));
59+
const properties = Object.getOwnPropertyDescriptors(obj);
60+
for (const { value } of Object.values(properties)) {
61+
attachInspectMethods(value);
3762
}
3863

39-
if (isObject(value)) {
40-
return formatObject(value);
64+
// Add obj[util.inspect.custom] if it does not exist and we can provide it.
65+
const bsontype = obj._bsontype;
66+
const stringifier = bsonStringifiers[bsontype];
67+
if (bsontype &&
68+
stringifier &&
69+
!(properties as any)[customInspect] &&
70+
!Object.isSealed(obj)) {
71+
tryAddInspect(obj, stringifier);
72+
} else if (isDate(obj)) {
73+
tryAddInspect(obj, function(this: Date): string {
74+
return this.toISOString();
75+
});
4176
}
42-
return value;
4377
}
4478

4579
export function inspect(value: any): string {
46-
const viewValue = toViewValue(value);
47-
const stringifiedValue = utilInspect(viewValue, {
80+
attachInspectMethods(value);
81+
82+
const stringifiedValue = utilInspect(value, {
4883
customInspect: true,
4984
depth: 1000,
5085
breakLength: 0

0 commit comments

Comments
 (0)