Skip to content

Commit 10f4267

Browse files
feat: show simpler uuid format VSCODE-470 (#701)
--------- Co-authored-by: Anna Henningsen <[email protected]>
1 parent 8210f7a commit 10f4267

File tree

5 files changed

+254
-7
lines changed

5 files changed

+254
-7
lines changed

src/editors/mongoDBDocumentService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type * as vscode from 'vscode';
2-
import { EJSON } from 'bson';
32
import type { Document } from 'bson';
43

54
import type ConnectionController from '../connectionController';
@@ -9,6 +8,7 @@ import type { EditDocumentInfo } from '../types/editDocumentInfoType';
98
import formatError from '../utils/formatError';
109
import type { StatusView } from '../views';
1110
import type TelemetryService from '../telemetry/telemetryService';
11+
import { getEJSON } from '../utils/ejson';
1212

1313
const log = createLogger('document controller');
1414

@@ -147,7 +147,7 @@ export default class MongoDBDocumentService {
147147
return;
148148
}
149149

150-
return JSON.parse(EJSON.stringify(documents[0]));
150+
return getEJSON(documents[0]);
151151
} catch (error) {
152152
this._statusView.hideMessage();
153153

src/language/worker.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { CliServiceProvider } from '@mongosh/service-provider-server';
2-
import { EJSON } from 'bson';
32
import { ElectronRuntime } from '@mongosh/browser-runtime-electron';
43
import { parentPort } from 'worker_threads';
54
import { ServerCommands } from './serverCommands';
@@ -10,6 +9,7 @@ import type {
109
MongoClientOptions,
1110
} from '../types/playgroundType';
1211
import util from 'util';
12+
import { getEJSON } from '../utils/ejson';
1313

1414
interface EvaluationResult {
1515
printable: any;
@@ -18,12 +18,12 @@ interface EvaluationResult {
1818

1919
const getContent = ({ type, printable }: EvaluationResult) => {
2020
if (type === 'Cursor' || type === 'AggregationCursor') {
21-
return JSON.parse(EJSON.stringify(printable.documents));
21+
return getEJSON(printable.documents);
2222
}
2323

2424
return typeof printable !== 'object' || printable === null
2525
? printable
26-
: JSON.parse(EJSON.stringify(printable));
26+
: getEJSON(printable);
2727
};
2828

2929
const getLanguage = (evaluationResult: EvaluationResult) => {

src/test/suite/editors/mongoDBDocumentService.test.ts

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,57 @@ suite('MongoDB Document Service Test Suite', () => {
8787
expect(document).to.be.deep.equal(newDocument);
8888
});
8989

90+
test('replaceDocument calls findOneAndReplace and saves a document when connected - extending the uuid type', async () => {
91+
const namespace = 'waffle.house';
92+
const connectionId = 'tasty_sandwhich';
93+
const documentId = '93333a0d-83f6-4e6f-a575-af7ea6187a4a';
94+
const document: { _id: string; myUuid?: { $uuid: string } } = {
95+
_id: '123',
96+
};
97+
const newDocument = {
98+
_id: '123',
99+
myUuid: {
100+
$binary: {
101+
base64: 'yO2rw/c4TKO2jauSqRR4ow==',
102+
subType: '04',
103+
},
104+
},
105+
};
106+
const source = DocumentSource.DOCUMENT_SOURCE_TREEVIEW;
107+
108+
const fakeActiveConnectionId = sandbox.fake.returns('tasty_sandwhich');
109+
sandbox.replace(
110+
testConnectionController,
111+
'getActiveConnectionId',
112+
fakeActiveConnectionId
113+
);
114+
115+
const fakeGetActiveDataService = sandbox.fake.returns({
116+
findOneAndReplace: () => {
117+
document.myUuid = { $uuid: 'c8edabc3-f738-4ca3-b68d-ab92a91478a3' };
118+
119+
return Promise.resolve(document);
120+
},
121+
});
122+
sandbox.replace(
123+
testConnectionController,
124+
'getActiveDataService',
125+
fakeGetActiveDataService
126+
);
127+
sandbox.stub(testStatusView, 'showMessage');
128+
sandbox.stub(testStatusView, 'hideMessage');
129+
130+
await testMongoDBDocumentService.replaceDocument({
131+
namespace,
132+
documentId,
133+
connectionId,
134+
newDocument,
135+
source,
136+
});
137+
138+
expect(document).to.be.deep.equal(document);
139+
});
140+
90141
test('fetchDocument calls find and returns a single document when connected', async () => {
91142
const namespace = 'waffle.house';
92143
const connectionId = 'tasty_sandwhich';
@@ -97,7 +148,7 @@ suite('MongoDB Document Service Test Suite', () => {
97148

98149
const fakeGetActiveDataService = sandbox.fake.returns({
99150
find: () => {
100-
return Promise.resolve([{ _id: '123' }]);
151+
return Promise.resolve(documents);
101152
},
102153
});
103154
sandbox.replace(
@@ -124,7 +175,60 @@ suite('MongoDB Document Service Test Suite', () => {
124175
source,
125176
});
126177

127-
expect(result).to.be.deep.equal(JSON.parse(EJSON.stringify(documents[0])));
178+
expect(result).to.be.deep.equal(EJSON.serialize(documents[0]));
179+
});
180+
181+
test('fetchDocument calls find and returns a single document when connected - simplifying the uuid type', async () => {
182+
const namespace = 'waffle.house';
183+
const connectionId = 'tasty_sandwhich';
184+
const documentId = '93333a0d-83f6-4e6f-a575-af7ea6187a4a';
185+
const line = 1;
186+
const documents = [
187+
{
188+
_id: '123',
189+
myUuid: {
190+
$binary: {
191+
base64: 'yO2rw/c4TKO2jauSqRR4ow==',
192+
subType: '04',
193+
},
194+
},
195+
},
196+
];
197+
const source = DocumentSource.DOCUMENT_SOURCE_PLAYGROUND;
198+
199+
const fakeGetActiveDataService = sandbox.fake.returns({
200+
find: () => {
201+
return Promise.resolve(documents);
202+
},
203+
});
204+
sandbox.replace(
205+
testConnectionController,
206+
'getActiveDataService',
207+
fakeGetActiveDataService
208+
);
209+
210+
const fakeGetActiveConnectionId = sandbox.fake.returns(connectionId);
211+
sandbox.replace(
212+
testConnectionController,
213+
'getActiveConnectionId',
214+
fakeGetActiveConnectionId
215+
);
216+
217+
sandbox.stub(testStatusView, 'showMessage');
218+
sandbox.stub(testStatusView, 'hideMessage');
219+
220+
const result = await testMongoDBDocumentService.fetchDocument({
221+
namespace,
222+
documentId,
223+
line,
224+
connectionId,
225+
source,
226+
});
227+
228+
expect(result).to.be.deep.equal({
229+
_id: '123',
230+
myUuid: { $uuid: 'c8edabc3-f738-4ca3-b68d-ab92a91478a3' },
231+
});
128232
});
129233

130234
test("if a user is not connected, documents won't be saved to MongoDB", async () => {

src/test/suite/utils/ejson.test.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { expect } from 'chai';
2+
import { getEJSON } from '../../../utils/ejson';
3+
4+
suite('getEJSON', function () {
5+
suite('Valid uuid', function () {
6+
const prettyUuid = {
7+
$uuid: '63b985b8-e8dd-4bda-9087-e4402f1a3ff5',
8+
};
9+
const rawUuid = {
10+
$binary: {
11+
base64: 'Y7mFuOjdS9qQh+RALxo/9Q==',
12+
subType: '04',
13+
},
14+
};
15+
16+
test('Simplifies top-level uuid', function () {
17+
const ejson = getEJSON({ uuid: rawUuid });
18+
expect(ejson).to.deep.equal({ uuid: prettyUuid });
19+
});
20+
21+
test('Simplifies nested uuid', function () {
22+
const ejson = getEJSON({
23+
grandparent: {
24+
parent: {
25+
sibling: 1,
26+
uuid: rawUuid,
27+
},
28+
},
29+
});
30+
expect(ejson).to.deep.equal({
31+
grandparent: {
32+
parent: {
33+
sibling: 1,
34+
uuid: prettyUuid,
35+
},
36+
},
37+
});
38+
});
39+
40+
test('Simplifies uuid in a nested array', function () {
41+
const ejson = getEJSON({
42+
items: [
43+
{
44+
parent: {
45+
sibling: 1,
46+
uuid: rawUuid,
47+
},
48+
},
49+
],
50+
});
51+
expect(ejson).to.deep.equal({
52+
items: [
53+
{
54+
parent: {
55+
sibling: 1,
56+
uuid: prettyUuid,
57+
},
58+
},
59+
],
60+
});
61+
});
62+
});
63+
64+
suite('Invalid uuid or not an uuid', function () {
65+
test('Ignores another subtype', function () {
66+
const document = {
67+
$binary: {
68+
base64: 'Y7mFuOjdS9qQh+RALxo/9Q==',
69+
subType: '02',
70+
},
71+
};
72+
const ejson = getEJSON(document);
73+
expect(ejson).to.deep.equal(document);
74+
});
75+
76+
test('Ignores invalid uuid', function () {
77+
const document = {
78+
$binary: {
79+
base64: 'Y7m==',
80+
subType: '04',
81+
},
82+
};
83+
const ejson = getEJSON(document);
84+
expect(ejson).to.deep.equal(document);
85+
});
86+
87+
test('Ignores null', function () {
88+
const document = {
89+
$binary: {
90+
base64: null,
91+
subType: '04',
92+
},
93+
};
94+
const ejson = getEJSON(document);
95+
expect(ejson).to.deep.equal(document);
96+
});
97+
});
98+
});

src/utils/ejson.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { EJSON } from 'bson';
2+
import type { Document } from 'bson';
3+
4+
const isObjectOrArray = (value: unknown) =>
5+
value !== null && typeof value === 'object';
6+
7+
function simplifyEJSON(item: Document[] | Document): Document {
8+
if (!isObjectOrArray(item)) return item;
9+
10+
if (Array.isArray(item)) {
11+
return item.map((arrayItem) =>
12+
isObjectOrArray(arrayItem) ? simplifyEJSON(arrayItem) : arrayItem
13+
);
14+
}
15+
16+
// UUIDs might be represented as {"$uuid": <canonical textual representation of a UUID>} in EJSON
17+
// Binary subtypes 3 or 4 are used to represent UUIDs in BSON
18+
// But, parsers MUST interpret the $uuid key as BSON Binary subtype 4
19+
// For this reason, we are applying this representation for subtype 4 only
20+
// see https://github.com/mongodb/specifications/blob/master/source/extended-json.rst#special-rules-for-parsing-uuid-fields
21+
if (
22+
item.$binary?.subType === '04' &&
23+
typeof item.$binary?.base64 === 'string'
24+
) {
25+
const hexString = Buffer.from(item.$binary.base64, 'base64').toString(
26+
'hex'
27+
);
28+
const match = /^(.{8})(.{4})(.{4})(.{4})(.{12})$/.exec(hexString);
29+
if (!match) return item;
30+
const asUUID = match.slice(1, 6).join('-');
31+
return { $uuid: asUUID };
32+
}
33+
34+
return Object.fromEntries(
35+
Object.entries(item).map(([key, value]) => [
36+
key,
37+
isObjectOrArray(value) ? simplifyEJSON(value) : value,
38+
])
39+
);
40+
}
41+
42+
export function getEJSON(item: Document[] | Document) {
43+
const ejson = EJSON.serialize(item);
44+
return simplifyEJSON(ejson);
45+
}

0 commit comments

Comments
 (0)