diff --git a/.evergreen/.install_node b/.evergreen/.install_node
index f0e8f43e99..4c3984aabd 100644
--- a/.evergreen/.install_node
+++ b/.evergreen/.install_node
@@ -1,7 +1,8 @@
+set -e
export NODE_JS_VERSION='12.18.4'
+export NVM_DIR="$HOME/.nvm"
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash
-export NVM_DIR="$HOME/.nvm"
echo "Setting NVM environment home: $NVM_DIR"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
diff --git a/.evergreen/.setup_env b/.evergreen/.setup_env
index d0f0e8a05b..89a193e284 100755
--- a/.evergreen/.setup_env
+++ b/.evergreen/.setup_env
@@ -1,3 +1,4 @@
+set -e
export NVM_DIR="$HOME/.nvm"
echo "Setting NVM environment home: $NVM_DIR"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
diff --git a/packages/browser-repl/src/components/shell-output-line.spec.tsx b/packages/browser-repl/src/components/shell-output-line.spec.tsx
index 9fa42dbe58..d6c921640a 100644
--- a/packages/browser-repl/src/components/shell-output-line.spec.tsx
+++ b/packages/browser-repl/src/components/shell-output-line.spec.tsx
@@ -149,7 +149,7 @@ describe('', () => {
}
}} />);
- expect(wrapper.text()).to.include('---');
+ expect(wrapper.find('hr')).to.have.lengthOf(1);
expect(wrapper.text()).to.include('metadata');
});
diff --git a/packages/browser-repl/src/components/shell-output-line.tsx b/packages/browser-repl/src/components/shell-output-line.tsx
index 2ae0fe5124..8a3d86b961 100644
--- a/packages/browser-repl/src/components/shell-output-line.tsx
+++ b/packages/browser-repl/src/components/shell-output-line.tsx
@@ -13,9 +13,9 @@ import { ShowCollectionsOutput } from './types/show-collections-output';
import { CursorOutput } from './types/cursor-output';
import { CursorIterationResultOutput } from './types/cursor-iteration-result-output';
import { ObjectOutput } from './types/object-output';
+import { StatsResultOutput } from './types/stats-result-output';
import { SimpleTypeOutput } from './types/simple-type-output';
import { ErrorOutput } from './types/error-output';
-import { inspect } from './utils/inspect';
import { ShowProfileOutput } from './types/show-profile-output';
const styles = require('./shell-output-line.less');
@@ -47,6 +47,10 @@ export class ShellOutputLine extends Component {
return {value}
;
}
+ if (typeof value === 'string' && type !== null) {
+ return ;
+ }
+
if (this.isPrimitiveOrFunction(value)) {
return ;
}
@@ -60,10 +64,7 @@ export class ShellOutputLine extends Component {
}
if (type === 'StatsResult') {
- const res = Object.keys(value).map(c => {
- return `${c}\n${inspect(value[c])}`;
- }).join('\n---\n');
- return ;
+ return ;
}
if (type === 'ListCommandsResult)') {
diff --git a/packages/browser-repl/src/components/shell-output.spec.tsx b/packages/browser-repl/src/components/shell-output.spec.tsx
index c5fd6a994f..0c79c16689 100644
--- a/packages/browser-repl/src/components/shell-output.spec.tsx
+++ b/packages/browser-repl/src/components/shell-output.spec.tsx
@@ -17,6 +17,12 @@ describe('', () => {
expect(wrapper.find(ShellOutputLine)).to.have.lengthOf(1);
});
+ it('renders no output lines if only one with a value of undefined is passed', () => {
+ const line1: ShellOutputEntry = { type: 'output', value: undefined };
+ const wrapper = shallow();
+ expect(wrapper.find(ShellOutputLine)).to.have.lengthOf(0);
+ });
+
it('pass the entry to the output line as prop', () => {
const line1: ShellOutputEntry = { type: 'output', value: 'line 1' };
const wrapper = shallow();
diff --git a/packages/browser-repl/src/components/shell-output.tsx b/packages/browser-repl/src/components/shell-output.tsx
index 743d969a76..dd2821bd97 100644
--- a/packages/browser-repl/src/components/shell-output.tsx
+++ b/packages/browser-repl/src/components/shell-output.tsx
@@ -18,6 +18,6 @@ export class ShellOutput extends Component {
};
render(): JSX.Element[] {
- return this.props.output.map(this.renderLine);
+ return this.props.output.filter(entry => entry.value !== undefined).map(this.renderLine);
}
}
diff --git a/packages/browser-repl/src/components/types/simple-type-output.tsx b/packages/browser-repl/src/components/types/simple-type-output.tsx
index 67be70e23b..31888b91b2 100644
--- a/packages/browser-repl/src/components/types/simple-type-output.tsx
+++ b/packages/browser-repl/src/components/types/simple-type-output.tsx
@@ -5,15 +5,18 @@ import { inspect } from '../utils/inspect';
interface SimpleTypeOutputProps {
value: any;
+ raw?: boolean;
}
export class SimpleTypeOutput extends Component {
static propTypes = {
- value: PropTypes.any
+ value: PropTypes.any,
+ raw: PropTypes.bool
};
render(): JSX.Element {
- return ();
+ const asString = this.props.raw ? this.props.value : inspect(this.props.value);
+ return ();
}
}
diff --git a/packages/browser-repl/src/components/types/stats-result-output.tsx b/packages/browser-repl/src/components/types/stats-result-output.tsx
new file mode 100644
index 0000000000..52ea9964a1
--- /dev/null
+++ b/packages/browser-repl/src/components/types/stats-result-output.tsx
@@ -0,0 +1,27 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { ObjectOutput } from './object-output';
+
+interface StatsResultOutputProps {
+ value: Record;
+}
+
+export class StatsResultOutput extends Component {
+ static propTypes = {
+ value: PropTypes.any
+ };
+
+ render(): JSX.Element {
+ const result: JSX.Element[] = [];
+ for (const [ key, value ] of Object.entries(this.props.value)) {
+ if (result.length > 0) {
+ result.push(
);
+ }
+ result.push(
+
{key}
+
+
);
+ }
+ return {result}
;
+ }
+}
diff --git a/packages/browser-repl/src/components/utils/inspect.spec.ts b/packages/browser-repl/src/components/utils/inspect.spec.ts
index 9162e5e402..1f7f1984ab 100644
--- a/packages/browser-repl/src/components/utils/inspect.spec.ts
+++ b/packages/browser-repl/src/components/utils/inspect.spec.ts
@@ -37,13 +37,46 @@ describe('inspect', () => {
inspect(undefined)
).to.equal('undefined');
});
+
+ it('inspects Dates', () => {
+ expect(
+ inspect(new Date('2020-11-06T14:26:29.131Z'))
+ ).to.equal('2020-11-06T14:26:29.131Z');
+ });
});
context('with BSON types', () => {
it('inspects ObjectId', () => {
expect(
inspect(new bson.ObjectId('0000007b3db627730e26fd0b'))
- ).to.equal('ObjectID("0000007b3db627730e26fd0b")');
+ ).to.equal('ObjectId("0000007b3db627730e26fd0b")');
+ });
+
+ it('inspects UUID', () => {
+ expect(
+ inspect(new bson.Binary('abcdefghiklmnopq', 4))
+ ).to.equal('UUID("61626364-6566-6768-696b-6c6d6e6f7071")');
+ });
+
+ it('inspects nested ObjectId', () => {
+ expect(
+ inspect({ p: new bson.ObjectId('0000007b3db627730e26fd0b') })
+ ).to.equal('{ p: ObjectId("0000007b3db627730e26fd0b") }');
+ });
+
+ it('inspects nested UUID', () => {
+ expect(
+ inspect({ p: new bson.Binary('abcdefghiklmnopq', 4) })
+ ).to.equal('{ p: UUID("61626364-6566-6768-696b-6c6d6e6f7071") }');
+ });
+
+ it('does not require BSON types to be instances of the current bson library', () => {
+ expect(
+ inspect({
+ _bsontype: 'ObjectID',
+ toHexString() { return '0000007b3db627730e26fd0b'; }
+ })
+ ).to.equal('ObjectId("0000007b3db627730e26fd0b")');
});
});
@@ -56,4 +89,13 @@ describe('inspect', () => {
});
});
});
+
+ context('with frozen objects with _bsontype properties', () => {
+ expect(
+ () => inspect(Object.freeze({
+ _bsontype: 'ObjectID',
+ toHexString() { return '0000007b3db627730e26fd0b'; }
+ }))
+ ).not.to.throw;
+ });
});
diff --git a/packages/browser-repl/src/components/utils/inspect.ts b/packages/browser-repl/src/components/utils/inspect.ts
index 7402f3b5f2..17485b1254 100644
--- a/packages/browser-repl/src/components/utils/inspect.ts
+++ b/packages/browser-repl/src/components/utils/inspect.ts
@@ -1,50 +1,85 @@
import { inspect as utilInspect } from 'util';
-type BSONBaseType = { _bsontype: string };
+import { bsonStringifiers } from '@mongosh/service-provider-core';
-const formatBsonType = (value: BSONBaseType): any => ({
- inspect(): string {
- return `${value._bsontype}(${(JSON.stringify(value))})`;
- }
-});
+// At the time of writing, the Compass dist package contains what appear to be
+// 155 different copies of the 'bson' module. It is impractical to attach
+// our inspection methods to each of those copies individually, like we do when
+// we are inside cli-repl.
+// Instead, we look for values with a _bsontype property inside the object graph
+// before printing them here, and attach inspection methods to each of them
+// individually.
+// This is not particularly fast, but should work just fine for user-facing
+// interfaces like printing shell output in the browser.
-function isBsonType(value: any): boolean {
- return !!(value && value._bsontype);
-}
+const customInspect = utilInspect.custom || 'inspect';
+const visitedObjects = new WeakSet();
-function isObject(value: any): boolean {
- return !!(value && typeof value === 'object' && !Array.isArray(value));
+function tryAddInspect(obj: any, stringifier: (this: any) => string): void {
+ try {
+ Object.defineProperty(obj, customInspect, {
+ writable: true,
+ configurable: true,
+ enumerable: false,
+ value: function() {
+ try {
+ return stringifier.call(this);
+ } catch (err) {
+ console.warn('Could not inspect bson object', { obj: this, err });
+ return utilInspect(this, { customInspect: false });
+ }
+ }
+ });
+ } catch (err) {
+ console.warn('Could not add inspect key to object', { obj, err });
+ }
}
-function formatProperty(value: any): any {
- if (isObject(value) && isBsonType(value)) {
- return formatBsonType(value);
+function isDate(value: any): boolean {
+ try {
+ Date.prototype.getTime.call(value);
+ return true;
+ } catch {
+ return false;
}
-
- return value;
}
-function formatObject(object: any): any {
- const viewObject: any = {};
- for (const key of Object.keys(object)) {
- viewObject[key] = formatProperty(object[key]);
+function attachInspectMethods(obj: any): void {
+ if ((typeof obj !== 'object' && typeof obj !== 'function') || obj === null) {
+ // Ignore primitives
+ return;
}
- return viewObject;
-}
-function toViewValue(value: any): any {
- if (isBsonType(value)) {
- return formatBsonType(value);
+ if (visitedObjects.has(obj)) {
+ return;
+ }
+ visitedObjects.add(obj);
+
+ // Traverse the rest of the object graph.
+ attachInspectMethods(Object.getPrototypeOf(obj));
+ const properties = Object.getOwnPropertyDescriptors(obj);
+ for (const { value } of Object.values(properties)) {
+ attachInspectMethods(value);
}
- if (isObject(value)) {
- return formatObject(value);
+ // Add obj[util.inspect.custom] if it does not exist and we can provide it.
+ const bsontype = obj._bsontype;
+ const stringifier = bsonStringifiers[bsontype];
+ if (bsontype &&
+ stringifier &&
+ !(properties as any)[customInspect] &&
+ !Object.isSealed(obj)) {
+ tryAddInspect(obj, stringifier);
+ } else if (isDate(obj)) {
+ tryAddInspect(obj, function(this: Date): string {
+ return this.toISOString();
+ });
}
- return value;
}
export function inspect(value: any): string {
- const viewValue = toViewValue(value);
- const stringifiedValue = utilInspect(viewValue, {
+ attachInspectMethods(value);
+
+ const stringifiedValue = utilInspect(value, {
customInspect: true,
depth: 1000,
breakLength: 0
diff --git a/packages/service-provider-core/src/index.ts b/packages/service-provider-core/src/index.ts
index 162aea82af..b1fb2ec810 100644
--- a/packages/service-provider-core/src/index.ts
+++ b/packages/service-provider-core/src/index.ts
@@ -17,6 +17,7 @@ import generateUri, { Scheme } from './uri-generator';
const DEFAULT_DB = 'test';
import * as bson from 'bson';
import ServiceProviderBulkOp, { ServiceProviderBulkFindOp, BulkBatch } from './bulk';
+import makePrintableBson, { bsonStringifiers } from './printable-bson';
export {
ServiceProvider,
@@ -42,6 +43,8 @@ export {
DEFAULT_DB,
ServiceProviderCore,
bson,
+ makePrintableBson,
+ bsonStringifiers,
ServiceProviderBulkFindOp,
ServiceProviderBulkOp,
BulkBatch
diff --git a/packages/service-provider-core/src/printable-bson.spec.ts b/packages/service-provider-core/src/printable-bson.spec.ts
new file mode 100644
index 0000000000..57604092cb
--- /dev/null
+++ b/packages/service-provider-core/src/printable-bson.spec.ts
@@ -0,0 +1,64 @@
+import { expect } from 'chai';
+import * as bson from 'bson';
+import { inspect } from 'util';
+import { makePrintableBson } from './';
+
+describe('BSON printers', function() {
+ before('make BSON objects printable', function() {
+ makePrintableBson(bson);
+ });
+
+ // Enable after https://github.com/mongodb/js-bson/pull/412
+ xit('formats ObjectIDs correctly', function() {
+ expect(inspect(new bson.ObjectId('5fa5694f88211043b23c7f11')))
+ .to.equal('ObjectId("5fa5694f88211043b23c7f11")');
+ });
+
+ it('formats DBRefs correctly', function() {
+ expect(inspect(new bson.DBRef('a', new bson.ObjectId('5f16b8bebe434dc98cdfc9cb'), 'db')))
+ .to.equal('DBRef("a", "5f16b8bebe434dc98cdfc9cb", "db")');
+ });
+
+ it('formats MinKey and MaxKey correctly', function() {
+ expect(inspect(new bson.MinKey())).to.equal('{ "$minKey" : 1 }');
+ expect(inspect(new bson.MaxKey())).to.equal('{ "$maxKey" : 1 }');
+ });
+
+ it('formats NumberInt correctly', function() {
+ expect(inspect(new bson.Int32(32))).to.equal('NumberInt(32)');
+ });
+
+ it('formats NumberLong correctly', function() {
+ expect(inspect(bson.Long.fromString('64'))).to.equal('NumberLong("64")');
+ });
+
+ it('formats NumberDecimal correctly', function() {
+ expect(inspect(bson.Decimal128.fromString('1'))).to.equal('NumberDecimal("1")');
+ });
+
+ it('formats Timestamp correctly', function() {
+ expect(inspect(new bson.Timestamp(1, 100))).to.equal('Timestamp(1, 100)');
+ });
+
+ it('formats Symbol correctly', function() {
+ expect(inspect(new bson.BSONSymbol('abc'))).to.equal('"abc"');
+ });
+
+ it('formats Code correctly', function() {
+ expect(inspect(new bson.Code('abc'))).to.equal('{ "code" : "abc" }');
+ });
+
+ it('formats BinData correctly', function() {
+ expect(inspect(new bson.Binary('abc'))).to.equal('BinData(0, "YWJj")');
+ });
+
+ it('formats UUIDs correctly', function() {
+ expect(inspect(new bson.Binary(Buffer.from('0123456789abcdef0123456789abcdef', 'hex'), 4)))
+ .to.equal('UUID("01234567-89ab-cdef-0123-456789abcdef")');
+ });
+
+ it('formats MD5s correctly', function() {
+ expect(inspect(new bson.Binary(Buffer.from('0123456789abcdef0123456789abcdef', 'hex'), 5)))
+ .to.equal('MD5("0123456789abcdef0123456789abcdef")');
+ });
+});
diff --git a/packages/service-provider-core/src/printable-bson.ts b/packages/service-provider-core/src/printable-bson.ts
index 37a6cdef3b..04d22e051a 100644
--- a/packages/service-provider-core/src/printable-bson.ts
+++ b/packages/service-provider-core/src/printable-bson.ts
@@ -1,85 +1,64 @@
-import BSON from 'bson';
+import * as BSON from 'bson';
+const inspectCustom = Symbol.for('nodejs.util.inspect.custom');
-/**
- * This method modifies the BSON class passed in as argument. This is required so that
- * we can have the driver return our BSON classes without having to write our own serializer.
- * @param {Object} bson
- */
-export default function(bson?: any): void {
- if (!bson) {
- bson = BSON;
- }
- const toString = require('util').inspect.custom || 'inspect';
-
- bson.ObjectId.prototype[toString] = function(): string {
+export const bsonStringifiers: Record string> = {
+ ObjectID: function(): string {
return `ObjectId("${this.toHexString()}")`;
- };
- bson.ObjectId.prototype.asPrintable = bson.ObjectId.prototype[toString];
+ },
- bson.DBRef.prototype[toString] = function(): string {
+ DBRef: function(): string {
// NOTE: if OID is an ObjectId class it will just print the oid string.
return `DBRef("${this.namespace}", "${
this.oid === undefined || this.oid.toString === undefined ?
this.oid :
this.oid.toString()
}"${this.db ? `, "${this.db}"` : ''})`;
- };
- bson.DBRef.prototype.asPrintable = bson.DBRef.prototype[toString];
+ },
- bson.MaxKey.prototype[toString] = function(): string {
+ MaxKey: function(): string {
return '{ "$maxKey" : 1 }';
- };
- bson.MaxKey.prototype.asPrintable = bson.MaxKey.prototype[toString];
+ },
- bson.MinKey.prototype[toString] = function(): string {
+ MinKey: function(): string {
return '{ "$minKey" : 1 }';
- };
- bson.MinKey.prototype.asPrintable = bson.MinKey.prototype[toString];
+ },
- bson.Timestamp.prototype[toString] = function(): string {
+ Timestamp: function(): string {
return `Timestamp(${this.getLowBits().toString()}, ${this.getHighBits().toString()})`;
- };
- bson.Timestamp.prototype.asPrintable = bson.Timestamp.prototype[toString];
+ },
// The old shell could not print Symbols so this was undefined behavior
- if ('Symbol' in bson) {
- bson.Symbol.prototype[toString] = function(): string {
- return `"${this.valueOf()}"`;
- };
- }
- if ('BSONSymbol' in bson) {
- bson.BSONSymbol.prototype[toString] = function(): string {
- return `"${this.valueOf()}"`;
- };
- }
+ Symbol: function(): string {
+ return `"${this.valueOf()}"`;
+ },
+
+ BSONSymbol: function(): string {
+ return `"${this.valueOf()}"`;
+ },
- bson.Code.prototype[toString] = function(): string {
+ Code: function(): string {
const j = this.toJSON();
return `{ "code" : "${j.code}"${j.scope ? `, "scope" : ${JSON.stringify(j.scope)}` : ''} }`;
- };
- bson.Code.prototype.asPrintable = bson.Code.prototype[toString];
+ },
- bson.Decimal128.prototype[toString] = function(): string {
+ Decimal128: function(): string {
return `NumberDecimal("${this.toString()}")`;
- };
- bson.Decimal128.prototype.asPrintable = bson.Decimal128.prototype[toString];
+ },
- bson.Int32.prototype[toString] = function(): string {
+ Int32: function(): string {
return `NumberInt(${this.valueOf()})`;
- };
- bson.Int32.prototype.asPrintable = bson.Int32.prototype[toString];
+ },
- bson.Long.prototype[toString] = function(): string {
+ Long: function(): string {
return `NumberLong("${this.toString()}")`;
- };
- bson.Long.prototype.asPrintable = bson.Long.prototype[toString];
+ },
- bson.Binary.prototype[toString] = function(): string {
+ Binary: function(): string {
const asBuffer = this.value(true);
switch (this.sub_type) {
- case bson.Binary.SUBTYPE_MD5:
+ case BSON.Binary.SUBTYPE_MD5:
return `MD5("${asBuffer.toString('hex')}")`;
- case bson.Binary.SUBTYPE_UUID:
+ case BSON.Binary.SUBTYPE_UUID:
if (asBuffer.length === 16) {
// Format '0123456789abcdef0123456789abcdef' into
// '01234567-89ab-cdef-0123-456789abcdef'.
@@ -94,6 +73,34 @@ export default function(bson?: any): void {
default:
return `BinData(${this.sub_type}, "${asBuffer.toString('base64')}")`;
}
- };
- bson.Binary.prototype.asPrintable = bson.Binary.prototype[toString];
+ },
+};
+bsonStringifiers.ObjectId = bsonStringifiers.ObjectID;
+
+/**
+ * This method modifies the BSON class passed in as argument. This is required so that
+ * we can have the driver return our BSON classes without having to write our own serializer.
+ * @param {Object} bson
+ */
+export default function(bson?: any): void {
+ if (!bson) {
+ bson = BSON;
+ }
+
+ for (const [ key, stringifier ] of Object.entries(bsonStringifiers)) {
+ if (!(key in bson)) {
+ continue;
+ }
+ const cls = bson[key];
+ for (const key of [inspectCustom, 'inspect']) {
+ try {
+ (cls as any).prototype[key] = stringifier;
+ } catch {
+ // This may fail because bson.ObjectId.prototype[toString] can exist as a
+ // read-only property. https://github.com/mongodb/js-bson/pull/412 takes
+ // care of this. In the CLI repl and Compass this still works fine, because
+ // those are on bson@1.x.
+ }
+ }
+ }
}
diff --git a/packages/shell-api/src/shell-bson.ts b/packages/shell-api/src/shell-bson.ts
index f8b0de76b4..ab55caac0e 100644
--- a/packages/shell-api/src/shell-bson.ts
+++ b/packages/shell-api/src/shell-bson.ts
@@ -1,6 +1,6 @@
import { ALL_PLATFORMS, ALL_SERVER_VERSIONS, ALL_TOPOLOGIES, ServerVersions } from './enums';
import Help from './help';
-import { bson as BSON } from '@mongosh/service-provider-core';
+import { bson as BSON, makePrintableBson } from '@mongosh/service-provider-core';
import { MongoshInternalError, MongoshInvalidInputError } from '@mongosh/errors';
import { assertArgsDefined, assertArgsType } from './helpers';
@@ -36,7 +36,9 @@ export default function constructShellBson(bson: any): any {
if (bson === undefined) {
bson = BSON;
}
+ makePrintableBson(bson);
const oldBSON = 'Symbol' in bson;
+
const helps: any = {};
[
'Binary', 'Code', 'DBRef', 'Decimal128', 'Int32', 'Long', 'MaxKey', 'MinKey', 'ObjectId', 'Timestamp', 'Map'