diff --git a/playgrounds/different-id-types.mongodb b/playgrounds/different-id-types.mongodb new file mode 100644 index 000000000..7011773bb --- /dev/null +++ b/playgrounds/different-id-types.mongodb @@ -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' +}); diff --git a/playgrounds/index-types.mongodb b/playgrounds/index-types.mongodb index 2bdebfffb..9c200e90d 100644 --- a/playgrounds/index-types.mongodb +++ b/playgrounds/index-types.mongodb @@ -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 diff --git a/src/editors/editorsController.ts b/src/editors/editorsController.ts index 408e0c0fe..5669fe78d 100644 --- a/src/editors/editorsController.ts +++ b/src/editors/editorsController.ts @@ -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. @@ -108,14 +153,6 @@ export default class EditorsController { async openMongoDBDocument(data: EditDocumentInfo): Promise { 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; @@ -128,8 +165,6 @@ export default class EditorsController { return false; } - this._saveDocumentToMemoryFileSystem(fileName, mdbDocument); - const activeConnectionId = this._connectionController.getActiveConnectionId() || ''; const namespaceUriQuery = `${NAMESPACE_URI_IDENTIFIER}=${data.namespace}`; @@ -137,10 +172,20 @@ export default class EditorsController { 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 }); @@ -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 { 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 @@ -294,7 +321,7 @@ export default class EditorsController { ); } - const uri = EditorsController.getViewCollectionDocumentsUri( + const uri = getViewCollectionDocumentsUri( operationId, namespace, connectionId @@ -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 } ); diff --git a/src/test/suite/editors/editorsController.test.ts b/src/test/suite/editors/editorsController.test.ts index feaa32494..1f5ee2d3f 100644 --- a/src/test/suite/editors/editorsController.test.ts +++ b/src/test/suite/editors/editorsController.test.ts @@ -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; @@ -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', ); }); diff --git a/src/test/suite/explorer/playgroundsExplorer.test.ts b/src/test/suite/explorer/playgroundsExplorer.test.ts index b27723099..a13780c69 100644 --- a/src/test/suite/explorer/playgroundsExplorer.test.ts +++ b/src/test/suite/explorer/playgroundsExplorer.test.ts @@ -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);