Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(data-explorer): opening documents with _ids with slashes VSCODE-276 #342

Merged
merged 4 commits into from
Oct 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions playgrounds/different-id-types.mongodb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Playground for seeding the `mongodbVSCodePlaygroundDB.idTypeTesting` with
// documents that have different kinds of ids.

const databaseName = 'mongodbVSCodePlaygroundDB';
const collectionName = 'idTypeTesting';

use(databaseName);

db[collectionName].insertOne({
description: 'auto generated default object id'
});

db[collectionName].insertOne({
_id: ObjectId(),
description: 'object id'
});

db[collectionName].insertOne({
_id: 'testString',
description: 'string'
});

db[collectionName].insertOne({
_id: 123,
description: 'number'
});

db[collectionName].insertOne({
_id: {
name: 'aaa'
},
description: 'object'
});

db[collectionName].insertOne({
_id: 'abc//\\\nab c$%@1s df',
description: 'string with special characters'
});

db[collectionName].insertOne({
_id: {
name: 'abc//\\\nab c$%@1s df',
2: 3
},
description: 'object with a string with special characters'
});

db[collectionName].insertOne({
_id: new Date(),
description: 'date'
});

db[collectionName].insertOne({
_id: Binary('pineapple'),
description: 'binary'
});
8 changes: 2 additions & 6 deletions playgrounds/index-types.mongodb
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
// MongoDB Playground
// To disable this template go to Settings | MongoDB | Use Default Template For Playground.
// Make sure you are connected to enable completions and to be able to run a playground.
// Use Ctrl+Space inside a snippet or a string literal to trigger completions.
// Playground for seeding the `mongodbVSCodePlaygroundDB.index-testing` with
// different types of indexes.

// Select the database to use.
use('mongodbVSCodePlaygroundDB');

db['index-testing'].insertOne({
'title': 'there and back again'
});

// Insert a few documents into the sales collection.
db['index-testing'].createIndex({
'fieldAscending': 1,
'fieldDescending': -1
Expand Down
95 changes: 61 additions & 34 deletions src/editors/editorsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,51 @@ import TelemetryService from '../telemetry/telemetryService';

const log = createLogger('editors controller');

export function getFileDisplayNameForDocument(
documentId: EJSON.SerializableTypes,
namespace: string
) {
let displayName = `${namespace}:${EJSON.stringify(documentId)}`;

// Encode special file uri characters to ensure VSCode handles
// it correctly in a uri while avoiding collisions.
displayName = displayName.replace(/[\\/%]/gi, function(c) {
return `%${c.charCodeAt(0).toString(16)}`;
});

displayName = displayName.length > 200
? displayName.substring(0, 200)
: displayName;

return displayName;
}

export function getViewCollectionDocumentsUri(
operationId: string,
namespace: string,
connectionId: string
): vscode.Uri {
// We attach a unique id to the query so that it creates a new file in
// the editor and so that we can virtually manage the amount of docs shown.
const operationIdUriQuery = `${OPERATION_ID_URI_IDENTIFIER}=${operationId}`;
const connectionIdUriQuery = `${CONNECTION_ID_URI_IDENTIFIER}=${connectionId}`;
const namespaceUriQuery = `${NAMESPACE_URI_IDENTIFIER}=${namespace}`;
const uriQuery = `?${namespaceUriQuery}&${connectionIdUriQuery}&${operationIdUriQuery}`;

// Encode special file uri characters to ensure VSCode handles
// it correctly in a uri while avoiding collisions.
const namespaceDisplayName = encodeURIComponent(
namespace.replace(/[\\/%]/gi, function(c) {
return `%${c.charCodeAt(0).toString(16)}`;
})
);

// The part of the URI after the scheme and before the query is the file name.
return vscode.Uri.parse(
`${VIEW_COLLECTION_SCHEME}:Results: ${namespaceDisplayName}.json${uriQuery}`
);
}

/**
* This controller manages when our extension needs to open
* new editors and the data they need. It also manages active editors.
Expand Down Expand Up @@ -108,14 +153,6 @@ export default class EditorsController {

async openMongoDBDocument(data: EditDocumentInfo): Promise<boolean> {
try {
let fileDocumentId = EJSON.stringify(data.documentId);

fileDocumentId =
fileDocumentId.length > 50
? fileDocumentId.substring(0, 50)
: fileDocumentId;

const fileName = `${VIEW_DOCUMENT_SCHEME}:/${data.namespace}:${fileDocumentId}.json`;
const mdbDocument = (await this._mongoDBDocumentService.fetchDocument(
data
)) as EJSON.SerializableTypes;
Expand All @@ -128,19 +165,27 @@ export default class EditorsController {
return false;
}

this._saveDocumentToMemoryFileSystem(fileName, mdbDocument);

const activeConnectionId =
this._connectionController.getActiveConnectionId() || '';
const namespaceUriQuery = `${NAMESPACE_URI_IDENTIFIER}=${data.namespace}`;
const connectionIdUriQuery = `${CONNECTION_ID_URI_IDENTIFIER}=${activeConnectionId}`;
const documentIdReference = this._documentIdStore.add(data.documentId);
const documentIdUriQuery = `${DOCUMENT_ID_URI_IDENTIFIER}=${documentIdReference}`;
const documentSourceUriQuery = `${DOCUMENT_SOURCE_URI_IDENTIFIER}=${data.source}`;
const uri: vscode.Uri = vscode.Uri.parse(fileName).with({

const fileTitle = encodeURIComponent(getFileDisplayNameForDocument(
data.documentId,
data.namespace
));
const fileName = `${VIEW_DOCUMENT_SCHEME}:/${fileTitle}.json`;

const fileUri = vscode.Uri.parse(fileName, true).with({
query: `?${namespaceUriQuery}&${connectionIdUriQuery}&${documentIdUriQuery}&${documentSourceUriQuery}`
});
const document = await vscode.workspace.openTextDocument(uri);

this._saveDocumentToMemoryFileSystem(fileUri, mdbDocument);

const document = await vscode.workspace.openTextDocument(fileUri);

await vscode.window.showTextDocument(document, { preview: false });

Expand Down Expand Up @@ -212,31 +257,13 @@ export default class EditorsController {
}
}

static getViewCollectionDocumentsUri(
operationId: string,
namespace: string,
connectionId: string
): vscode.Uri {
// We attach a unique id to the query so that it creates a new file in
// the editor and so that we can virtually manage the amount of docs shown.
const operationIdUriQuery = `${OPERATION_ID_URI_IDENTIFIER}=${operationId}`;
const connectionIdUriQuery = `${CONNECTION_ID_URI_IDENTIFIER}=${connectionId}`;
const namespaceUriQuery = `${NAMESPACE_URI_IDENTIFIER}=${namespace}`;
const uriQuery = `?${namespaceUriQuery}&${connectionIdUriQuery}&${operationIdUriQuery}`;

// The part of the URI after the scheme and before the query is the file name.
return vscode.Uri.parse(
`${VIEW_COLLECTION_SCHEME}:Results: ${namespace}.json${uriQuery}`
);
}

async onViewCollectionDocuments(namespace: string): Promise<boolean> {
log.info('view collection documents', namespace);

const operationId = this._collectionDocumentsOperationsStore.createNewOperation();
const activeConnectionId =
this._connectionController.getActiveConnectionId() || '';
const uri = EditorsController.getViewCollectionDocumentsUri(
const uri = getViewCollectionDocumentsUri(
operationId,
namespace,
activeConnectionId
Expand Down Expand Up @@ -294,7 +321,7 @@ export default class EditorsController {
);
}

const uri = EditorsController.getViewCollectionDocumentsUri(
const uri = getViewCollectionDocumentsUri(
operationId,
namespace,
connectionId
Expand All @@ -311,11 +338,11 @@ export default class EditorsController {
}

_saveDocumentToMemoryFileSystem(
fileName: string,
fileUri: vscode.Uri,
document: EJSON.SerializableTypes
): void {
this._memoryFileSystemProvider.writeFile(
vscode.Uri.parse(fileName),
fileUri,
Buffer.from(JSON.stringify(document, null, 2)),
{ create: true, overwrite: true }
);
Expand Down
72 changes: 60 additions & 12 deletions src/test/suite/editors/editorsController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import assert from 'assert';
import chai from 'chai';
import { mockTextEditor } from '../stubs';
import sinon from 'sinon';
import { ObjectId } from 'bson';

import { EditorsController } from '../../../editors';
import {
getFileDisplayNameForDocument,
getViewCollectionDocumentsUri
} from '../../../editors/editorsController';

const expect = chai.expect;

Expand All @@ -17,28 +21,72 @@ suite('Editors Controller Test Suite', () => {
sinon.restore();
});

suite('#getFileDisplayNameForDocumentId', () => {
test('it strips special characters from the document id', () => {
const str = 'abc//\\\nab c"$%%..@1s df""';
const result = getFileDisplayNameForDocument(str, 'a.b');
const expected = 'a.b:"abc%2f%2f%5c%5c%5cnab c%5c"$%25%25..@1s df%5c"%5c""';
assert.strictEqual(result, expected);
});

test('it trims the string to 200 characters', () => {
const str = '123sdfhadfbnjiekbfdakjsdbfkjsabdfkjasbdfkjsvasdjvbskdafdf123sdfhadfbnjiekbfdakjsdbfkjsabdfkjasbdfkjsvasdjvbskdafdffbnjiekbfdakjsdbfkjsabdfkjasbfbnjiekbfdakjsdbfkjsabdfkjasbkjasbfbnjiekbfdakjsdbfkjsabdfkjasb';
const result = getFileDisplayNameForDocument(str, 'db.col');
const expected = 'db.col:"123sdfhadfbnjiekbfdakjsdbfkjsabdfkjasbdfkjsvasdjvbskdafdf123sdfhadfbnjiekbfdakjsdbfkjsabdfkjasbdfkjsvasdjvbskdafdffbnjiekbfdakjsdbfkjsabdfkjasbfbnjiekbfdakjsdbfkjsabdfkjasbkjasbfbnjiekbfdakjsd';
assert.strictEqual(result, expected);
});

test('it handles ids that are objects', () => {
const str = {
str: 'abc//\\\nab c$%%..@1s df"',
b: new ObjectId('5d973ae744376d2aae72a160')
};
const result = getFileDisplayNameForDocument(str, 'db.col');
const expected = 'db.col:{"str":"abc%2f%2f%5c%5c%5cnab c$%25%25..@1s df%5c"","b":{"$oid":"5d973ae744376d2aae72a160"}}';
assert.strictEqual(result, expected);
});

test('has the namespace at the start of the display name', () => {
const str = 'pineapples';
const result = getFileDisplayNameForDocument(str, 'grilled');
const expected = 'grilled:"pineapples"';
assert.strictEqual(result, expected);
});
});

test('getViewCollectionDocumentsUri builds a uri from the namespace and connection info', () => {
const testOpId = '100011011101110011';
const testNamespace = 'myFavoriteNamespace';
const testConnectionId = 'alienSateliteConnection';
const testUri = EditorsController.getViewCollectionDocumentsUri(
const testUri = getViewCollectionDocumentsUri(
testOpId,
testNamespace,
testConnectionId
);

assert(
testUri.path === 'Results: myFavoriteNamespace.json',
`Expected uri path ${testUri.path} to equal 'Results: myFavoriteNamespace.json'.`
assert.strictEqual(testUri.path, 'Results: myFavoriteNamespace.json');
assert.strictEqual(testUri.scheme, 'VIEW_COLLECTION_SCHEME');
assert.strictEqual(
testUri.query,
'namespace=myFavoriteNamespace&connectionId=alienSateliteConnection&operationId=100011011101110011',
);
assert(
testUri.scheme === 'VIEW_COLLECTION_SCHEME',
`Expected uri scheme ${testUri.scheme} to equal 'VIEW_COLLECTION_SCHEME'.`
});

test('getViewCollectionDocumentsUri handles / \\ and % in the namespace', () => {
const testOpId = '100011011101110011';
const testNamespace = 'myFa%%\\\\///\\%vorite%Namespace';
const testConnectionId = 'alienSateliteConnection';
const testUri = getViewCollectionDocumentsUri(
testOpId,
testNamespace,
testConnectionId
);
assert(
testUri.query ===
'namespace=myFavoriteNamespace&connectionId=alienSateliteConnection&operationId=100011011101110011',
`Expected uri query ${testUri.query} to equal 'namespace=myFavoriteNamespace&connectionId=alienSateliteConnection&operationId=100011011101110011'.`

assert.strictEqual(testUri.path, 'Results: myFa%25%25%5c%5c%2f%2f%2f%5c%25vorite%25Namespace.json');
assert.strictEqual(testUri.scheme, 'VIEW_COLLECTION_SCHEME');
assert.strictEqual(
testUri.query,
'namespace=myFa%%\\\\///\\%vorite%Namespace&connectionId=alienSateliteConnection&operationId=100011011101110011',
);
});

Expand Down
14 changes: 8 additions & 6 deletions src/test/suite/explorer/playgroundsExplorer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,20 @@ suite('Playgrounds Controller Test Suite', function () {
try {
const children = await treeController.getPlaygrounds(rootUri);

assert(
Object.keys(children).length === 4,
`Tree playgrounds should have 4 child, found ${children.length}`
assert.strictEqual(
Object.keys(children).length,
5,
`Tree playgrounds should have 5 child, found ${children.length}`
);

const playgrounds = Object.values(children).filter(
(item: any) => item.label && item.label.split('.').pop() === 'mongodb'
);

assert(
Object.keys(playgrounds).length === 4,
`Tree playgrounds should have 4 playgrounds with mongodb extension, found ${children.length}`
assert.strictEqual(
Object.keys(playgrounds).length,
5,
`Tree playgrounds should have 5 playgrounds with mongodb extension, found ${children.length}`
);
} catch (error) {
assert(false, error);
Expand Down