Skip to content

Commit

Permalink
feat: show simpler uuid format VSCODE-470 (#701)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Anna Henningsen <anna.henningsen@mongodb.com>
  • Loading branch information
paula-stacho and addaleax authored Mar 13, 2024
1 parent 8210f7a commit 10f4267
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 7 deletions.
4 changes: 2 additions & 2 deletions src/editors/mongoDBDocumentService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type * as vscode from 'vscode';
import { EJSON } from 'bson';
import type { Document } from 'bson';

import type ConnectionController from '../connectionController';
Expand All @@ -9,6 +8,7 @@ import type { EditDocumentInfo } from '../types/editDocumentInfoType';
import formatError from '../utils/formatError';
import type { StatusView } from '../views';
import type TelemetryService from '../telemetry/telemetryService';
import { getEJSON } from '../utils/ejson';

const log = createLogger('document controller');

Expand Down Expand Up @@ -147,7 +147,7 @@ export default class MongoDBDocumentService {
return;
}

return JSON.parse(EJSON.stringify(documents[0]));
return getEJSON(documents[0]);
} catch (error) {
this._statusView.hideMessage();

Expand Down
6 changes: 3 additions & 3 deletions src/language/worker.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { CliServiceProvider } from '@mongosh/service-provider-server';
import { EJSON } from 'bson';
import { ElectronRuntime } from '@mongosh/browser-runtime-electron';
import { parentPort } from 'worker_threads';
import { ServerCommands } from './serverCommands';
Expand All @@ -10,6 +9,7 @@ import type {
MongoClientOptions,
} from '../types/playgroundType';
import util from 'util';
import { getEJSON } from '../utils/ejson';

interface EvaluationResult {
printable: any;
Expand All @@ -18,12 +18,12 @@ interface EvaluationResult {

const getContent = ({ type, printable }: EvaluationResult) => {
if (type === 'Cursor' || type === 'AggregationCursor') {
return JSON.parse(EJSON.stringify(printable.documents));
return getEJSON(printable.documents);
}

return typeof printable !== 'object' || printable === null
? printable
: JSON.parse(EJSON.stringify(printable));
: getEJSON(printable);
};

const getLanguage = (evaluationResult: EvaluationResult) => {
Expand Down
108 changes: 106 additions & 2 deletions src/test/suite/editors/mongoDBDocumentService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,57 @@ suite('MongoDB Document Service Test Suite', () => {
expect(document).to.be.deep.equal(newDocument);
});

test('replaceDocument calls findOneAndReplace and saves a document when connected - extending the uuid type', async () => {
const namespace = 'waffle.house';
const connectionId = 'tasty_sandwhich';
const documentId = '93333a0d-83f6-4e6f-a575-af7ea6187a4a';
const document: { _id: string; myUuid?: { $uuid: string } } = {
_id: '123',
};
const newDocument = {
_id: '123',
myUuid: {
$binary: {
base64: 'yO2rw/c4TKO2jauSqRR4ow==',
subType: '04',
},
},
};
const source = DocumentSource.DOCUMENT_SOURCE_TREEVIEW;

const fakeActiveConnectionId = sandbox.fake.returns('tasty_sandwhich');
sandbox.replace(
testConnectionController,
'getActiveConnectionId',
fakeActiveConnectionId
);

const fakeGetActiveDataService = sandbox.fake.returns({
findOneAndReplace: () => {
document.myUuid = { $uuid: 'c8edabc3-f738-4ca3-b68d-ab92a91478a3' };

return Promise.resolve(document);
},
});
sandbox.replace(
testConnectionController,
'getActiveDataService',
fakeGetActiveDataService
);
sandbox.stub(testStatusView, 'showMessage');
sandbox.stub(testStatusView, 'hideMessage');

await testMongoDBDocumentService.replaceDocument({
namespace,
documentId,
connectionId,
newDocument,
source,
});

expect(document).to.be.deep.equal(document);
});

test('fetchDocument calls find and returns a single document when connected', async () => {
const namespace = 'waffle.house';
const connectionId = 'tasty_sandwhich';
Expand All @@ -97,7 +148,7 @@ suite('MongoDB Document Service Test Suite', () => {

const fakeGetActiveDataService = sandbox.fake.returns({
find: () => {
return Promise.resolve([{ _id: '123' }]);
return Promise.resolve(documents);
},
});
sandbox.replace(
Expand All @@ -124,7 +175,60 @@ suite('MongoDB Document Service Test Suite', () => {
source,
});

expect(result).to.be.deep.equal(JSON.parse(EJSON.stringify(documents[0])));
expect(result).to.be.deep.equal(EJSON.serialize(documents[0]));
});

test('fetchDocument calls find and returns a single document when connected - simplifying the uuid type', async () => {
const namespace = 'waffle.house';
const connectionId = 'tasty_sandwhich';
const documentId = '93333a0d-83f6-4e6f-a575-af7ea6187a4a';
const line = 1;
const documents = [
{
_id: '123',
myUuid: {
$binary: {
base64: 'yO2rw/c4TKO2jauSqRR4ow==',
subType: '04',
},
},
},
];
const source = DocumentSource.DOCUMENT_SOURCE_PLAYGROUND;

const fakeGetActiveDataService = sandbox.fake.returns({
find: () => {
return Promise.resolve(documents);
},
});
sandbox.replace(
testConnectionController,
'getActiveDataService',
fakeGetActiveDataService
);

const fakeGetActiveConnectionId = sandbox.fake.returns(connectionId);
sandbox.replace(
testConnectionController,
'getActiveConnectionId',
fakeGetActiveConnectionId
);

sandbox.stub(testStatusView, 'showMessage');
sandbox.stub(testStatusView, 'hideMessage');

const result = await testMongoDBDocumentService.fetchDocument({
namespace,
documentId,
line,
connectionId,
source,
});

expect(result).to.be.deep.equal({
_id: '123',
myUuid: { $uuid: 'c8edabc3-f738-4ca3-b68d-ab92a91478a3' },
});
});

test("if a user is not connected, documents won't be saved to MongoDB", async () => {
Expand Down
98 changes: 98 additions & 0 deletions src/test/suite/utils/ejson.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { expect } from 'chai';
import { getEJSON } from '../../../utils/ejson';

suite('getEJSON', function () {
suite('Valid uuid', function () {
const prettyUuid = {
$uuid: '63b985b8-e8dd-4bda-9087-e4402f1a3ff5',
};
const rawUuid = {
$binary: {
base64: 'Y7mFuOjdS9qQh+RALxo/9Q==',
subType: '04',
},
};

test('Simplifies top-level uuid', function () {
const ejson = getEJSON({ uuid: rawUuid });
expect(ejson).to.deep.equal({ uuid: prettyUuid });
});

test('Simplifies nested uuid', function () {
const ejson = getEJSON({
grandparent: {
parent: {
sibling: 1,
uuid: rawUuid,
},
},
});
expect(ejson).to.deep.equal({
grandparent: {
parent: {
sibling: 1,
uuid: prettyUuid,
},
},
});
});

test('Simplifies uuid in a nested array', function () {
const ejson = getEJSON({
items: [
{
parent: {
sibling: 1,
uuid: rawUuid,
},
},
],
});
expect(ejson).to.deep.equal({
items: [
{
parent: {
sibling: 1,
uuid: prettyUuid,
},
},
],
});
});
});

suite('Invalid uuid or not an uuid', function () {
test('Ignores another subtype', function () {
const document = {
$binary: {
base64: 'Y7mFuOjdS9qQh+RALxo/9Q==',
subType: '02',
},
};
const ejson = getEJSON(document);
expect(ejson).to.deep.equal(document);
});

test('Ignores invalid uuid', function () {
const document = {
$binary: {
base64: 'Y7m==',
subType: '04',
},
};
const ejson = getEJSON(document);
expect(ejson).to.deep.equal(document);
});

test('Ignores null', function () {
const document = {
$binary: {
base64: null,
subType: '04',
},
};
const ejson = getEJSON(document);
expect(ejson).to.deep.equal(document);
});
});
});
45 changes: 45 additions & 0 deletions src/utils/ejson.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { EJSON } from 'bson';
import type { Document } from 'bson';

const isObjectOrArray = (value: unknown) =>
value !== null && typeof value === 'object';

function simplifyEJSON(item: Document[] | Document): Document {
if (!isObjectOrArray(item)) return item;

if (Array.isArray(item)) {
return item.map((arrayItem) =>
isObjectOrArray(arrayItem) ? simplifyEJSON(arrayItem) : arrayItem
);
}

// UUIDs might be represented as {"$uuid": <canonical textual representation of a UUID>} in EJSON
// Binary subtypes 3 or 4 are used to represent UUIDs in BSON
// But, parsers MUST interpret the $uuid key as BSON Binary subtype 4
// For this reason, we are applying this representation for subtype 4 only
// see https://github.com/mongodb/specifications/blob/master/source/extended-json.rst#special-rules-for-parsing-uuid-fields
if (
item.$binary?.subType === '04' &&
typeof item.$binary?.base64 === 'string'
) {
const hexString = Buffer.from(item.$binary.base64, 'base64').toString(
'hex'
);
const match = /^(.{8})(.{4})(.{4})(.{4})(.{12})$/.exec(hexString);
if (!match) return item;
const asUUID = match.slice(1, 6).join('-');
return { $uuid: asUUID };
}

return Object.fromEntries(
Object.entries(item).map(([key, value]) => [
key,
isObjectOrArray(value) ? simplifyEJSON(value) : value,
])
);
}

export function getEJSON(item: Document[] | Document) {
const ejson = EJSON.serialize(item);
return simplifyEJSON(ejson);
}

0 comments on commit 10f4267

Please sign in to comment.