From 10f42677deed55e0c2c394b665d75bb9615c4349 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Wed, 13 Mar 2024 16:37:20 +0100 Subject: [PATCH] feat: show simpler uuid format VSCODE-470 (#701) --------- Co-authored-by: Anna Henningsen --- src/editors/mongoDBDocumentService.ts | 4 +- src/language/worker.ts | 6 +- .../editors/mongoDBDocumentService.test.ts | 108 +++++++++++++++++- src/test/suite/utils/ejson.test.ts | 98 ++++++++++++++++ src/utils/ejson.ts | 45 ++++++++ 5 files changed, 254 insertions(+), 7 deletions(-) create mode 100644 src/test/suite/utils/ejson.test.ts create mode 100644 src/utils/ejson.ts diff --git a/src/editors/mongoDBDocumentService.ts b/src/editors/mongoDBDocumentService.ts index 387a4d401..928d02de0 100644 --- a/src/editors/mongoDBDocumentService.ts +++ b/src/editors/mongoDBDocumentService.ts @@ -1,5 +1,4 @@ import type * as vscode from 'vscode'; -import { EJSON } from 'bson'; import type { Document } from 'bson'; import type ConnectionController from '../connectionController'; @@ -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'); @@ -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(); diff --git a/src/language/worker.ts b/src/language/worker.ts index c1c73854a..f0f7ba819 100644 --- a/src/language/worker.ts +++ b/src/language/worker.ts @@ -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'; @@ -10,6 +9,7 @@ import type { MongoClientOptions, } from '../types/playgroundType'; import util from 'util'; +import { getEJSON } from '../utils/ejson'; interface EvaluationResult { printable: any; @@ -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) => { diff --git a/src/test/suite/editors/mongoDBDocumentService.test.ts b/src/test/suite/editors/mongoDBDocumentService.test.ts index f765d6b5b..dafbd5829 100644 --- a/src/test/suite/editors/mongoDBDocumentService.test.ts +++ b/src/test/suite/editors/mongoDBDocumentService.test.ts @@ -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'; @@ -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( @@ -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 () => { diff --git a/src/test/suite/utils/ejson.test.ts b/src/test/suite/utils/ejson.test.ts new file mode 100644 index 000000000..f36198442 --- /dev/null +++ b/src/test/suite/utils/ejson.test.ts @@ -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); + }); + }); +}); diff --git a/src/utils/ejson.ts b/src/utils/ejson.ts new file mode 100644 index 000000000..01b419d62 --- /dev/null +++ b/src/utils/ejson.ts @@ -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": } 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); +}