diff --git a/src/sql/azdata.d.ts b/src/sql/azdata.d.ts index 9d6c261605..5ed3b4a5b3 100644 --- a/src/sql/azdata.d.ts +++ b/src/sql/azdata.d.ts @@ -26,13 +26,21 @@ declare module 'azdata' { groupId: string; saveProfile: boolean; azureTenantId?: string; + options: { [name: string]: any }; static createFrom(options: any[]): ConnectionProfile; } /** * Get the current connection based on the active editor or Object Explorer selection - */ + */ export function getCurrentConnection(): Thenable; + + /** + * Get known connection profiles including active connections, recent connections and saved connections. + * @param activeConnectionsOnly Indicates whether only get the active connections, default value is false. + * @returns array of connections + */ + export function getConnections(activeConnectionsOnly?: boolean): Thenable; } } \ No newline at end of file diff --git a/src/sql/platform/connection/common/connectionManagement.ts b/src/sql/platform/connection/common/connectionManagement.ts index 13c7d92316..2363e1a6fe 100644 --- a/src/sql/platform/connection/common/connectionManagement.ts +++ b/src/sql/platform/connection/common/connectionManagement.ts @@ -282,6 +282,13 @@ export interface IConnectionManagementService { getProviderProperties(providerName: string): ConnectionProviderProperties; getConnectionIconId(connectionId: string): string; + + /** + * Get known connection profiles including active connections, recent connections and saved connections. + * @param activeConnectionsOnly Indicates whether only get the active connections, default value is false. + * @returns array of connections + */ + getConnections(activeConnectionsOnly?: boolean): ConnectionProfile[]; } export enum RunQueryOnConnectionMode { diff --git a/src/sql/platform/connection/common/connectionManagementService.ts b/src/sql/platform/connection/common/connectionManagementService.ts index 6fb2cd5901..d4f6814679 100644 --- a/src/sql/platform/connection/common/connectionManagementService.ts +++ b/src/sql/platform/connection/common/connectionManagementService.ts @@ -60,11 +60,7 @@ export class ConnectionManagementService extends Disposable implements IConnecti private _providers = new Map, properties: ConnectionProviderProperties }>(); private _iconProviders = new Map(); - private _uriToProvider: { [uri: string]: string; } = Object.create(null); - - private _connectionStatusManager = new ConnectionStatusManager(this._capabilitiesService, this._logService, this._environmentService, this._notificationService); - private _onAddConnectionProfile = new Emitter(); private _onDeleteConnectionProfile = new Emitter(); private _onConnect = new Emitter(); @@ -80,6 +76,7 @@ export class ConnectionManagementService extends Disposable implements IConnecti constructor( private _connectionStore: ConnectionStore, + private _connectionStatusManager: ConnectionStatusManager, @IConnectionDialogService private _connectionDialogService: IConnectionDialogService, @IServerGroupController private _serverGroupController: IServerGroupController, @IInstantiationService private _instantiationService: IInstantiationService, @@ -102,6 +99,9 @@ export class ConnectionManagementService extends Disposable implements IConnecti if (!this._connectionStore) { this._connectionStore = _instantiationService.createInstance(ConnectionStore); } + if (!this._connectionStatusManager) { + this._connectionStatusManager = new ConnectionStatusManager(this._capabilitiesService, this._logService, this._environmentService, this._notificationService); + } if (this._storageService) { this._mementoContext = new Memento(ConnectionManagementService.CONNECTION_MEMENTO, this._storageService); @@ -1432,4 +1432,54 @@ export class ConnectionManagementService extends Disposable implements IConnecti let connectionProvider = this._providers.get(providerName); return connectionProvider && connectionProvider.properties; } + + /** + * Get known connection profiles including active connections, recent connections and saved connections. + * @param activeConnectionsOnly Indicates whether only get the active connections, default value is false. + * @returns array of connections + **/ + public getConnections(activeConnectionsOnly?: boolean): ConnectionProfile[] { + + // 1. Active Connections + const connections = this.getActiveConnections(); + + const connectionExists: (conn: ConnectionProfile) => boolean = (conn) => { + return connections.find(existingConnection => existingConnection.id === conn.id) !== undefined; + }; + + if (!activeConnectionsOnly) { + // 2. Recent Connections + this.getRecentConnections().forEach(connection => { + if (!connectionExists(connection)) { + connections.push(connection); + } + }); + + // 3. Saved Connections + const groups = this.getConnectionGroups(); + if (groups && groups.length > 0) { + groups.forEach(group => { + this.getConnectionsInGroup(group).forEach(savedConnection => { + if (!connectionExists(savedConnection)) { + connections.push(savedConnection); + } + }); + }); + } + } + return connections; + } + + private getConnectionsInGroup(group: ConnectionProfileGroup): ConnectionProfile[] { + const connections = []; + if (group) { + if (group.connections && group.connections.length > 0) { + connections.push(...group.connections); + } + if (group.children && group.children.length > 0) { + group.children.forEach(child => connections.push(...this.getConnectionsInGroup(child))); + } + } + return connections; + } } diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index c661796174..13326030d4 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -544,6 +544,7 @@ export class ConnectionProfile { groupId: string; saveProfile: boolean; azureTenantId?: string; + options: { [name: string]: any }; static createFrom(options: any[]): ConnectionProfile { // create from options diff --git a/src/sql/workbench/api/node/extHostConnectionManagement.ts b/src/sql/workbench/api/node/extHostConnectionManagement.ts index a3cace9fc2..df39064353 100644 --- a/src/sql/workbench/api/node/extHostConnectionManagement.ts +++ b/src/sql/workbench/api/node/extHostConnectionManagement.ts @@ -26,6 +26,10 @@ export class ExtHostConnectionManagement extends ExtHostConnectionManagementShap return connection; } + public $getConnections(activeConnectionsOnly?: boolean): Thenable { + return this._proxy.$getConnections(activeConnectionsOnly); + } + // "sqlops" back-compat connection APIs public $getActiveConnections(): Thenable { return this._proxy.$getActiveConnections(); diff --git a/src/sql/workbench/api/node/mainThreadConnectionManagement.ts b/src/sql/workbench/api/node/mainThreadConnectionManagement.ts index d363d9f834..e11c6c01a2 100644 --- a/src/sql/workbench/api/node/mainThreadConnectionManagement.ts +++ b/src/sql/workbench/api/node/mainThreadConnectionManagement.ts @@ -18,6 +18,7 @@ import { generateUuid } from 'vs/base/common/uuid'; import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService'; import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; import { IConnectionDialogService } from 'sql/workbench/services/connection/common/connectionDialogService'; +import { deepClone } from 'vs/base/common/objects'; @extHostNamedCustomer(SqlMainContext.MainThreadConnectionManagement) export class MainThreadConnectionManagement implements MainThreadConnectionManagementShape { @@ -43,6 +44,10 @@ export class MainThreadConnectionManagement implements MainThreadConnectionManag this._toDispose = dispose(this._toDispose); } + public $getConnections(activeConnectionsOnly?: boolean): Thenable { + return Promise.resolve(this._connectionManagementService.getConnections(activeConnectionsOnly).map(profile => this.convertToConnectionProfile(profile))); + } + public $getActiveConnections(): Thenable { return Promise.resolve(this._connectionManagementService.getActiveConnections().map(profile => this.convertConnection(profile))); } @@ -117,6 +122,30 @@ export class MainThreadConnectionManagement implements MainThreadConnectionManag return connection; } + private convertToConnectionProfile(profile: IConnectionProfile): azdata.connection.ConnectionProfile { + if (!profile) { + return undefined; + } + + profile = this._connectionManagementService.removeConnectionProfileCredentials(profile); + let connection: azdata.connection.ConnectionProfile = { + providerId: profile.providerName, + connectionId: profile.id, + options: deepClone(profile.options), + connectionName: profile.connectionName, + serverName: profile.serverName, + databaseName: profile.databaseName, + userName: profile.userName, + password: profile.password, + authenticationType: profile.authenticationType, + savePassword: profile.savePassword, + groupFullName: profile.groupFullName, + groupId: profile.groupId, + saveProfile: profile.saveProfile + }; + return connection; + } + public $connect(connectionProfile: IConnectionProfile, saveConnection: boolean = true, showDashboard: boolean = true): Thenable { let profile = new ConnectionProfile(this._capabilitiesService, connectionProfile); profile.id = generateUuid(); diff --git a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts index bb50c5d952..8c0a5e5d9f 100644 --- a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts @@ -99,9 +99,13 @@ export function createApiFactory( getCurrentConnection(): Thenable { return extHostConnectionManagement.$getCurrentConnection(); }, + getConnections(activeConnectionsOnly?: boolean): Thenable { + return extHostConnectionManagement.$getConnections(activeConnectionsOnly); + }, // "sqlops" back-compat APIs getActiveConnections(): Thenable { + console.warn('the method azdata.connection.getActiveConnections has been deprecated, replace it with azdata.connection.getConnections'); return extHostConnectionManagement.$getActiveConnections(); }, getCredentials(connectionId: string): Thenable<{ [name: string]: string }> { diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index f1be903dd5..fff911764d 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -589,6 +589,7 @@ export interface MainThreadDataProtocolShape extends IDisposable { } export interface MainThreadConnectionManagementShape extends IDisposable { + $getConnections(activeConnectionsOnly?: boolean): Thenable; $getActiveConnections(): Thenable; $getCurrentConnection(): Thenable; $getCredentials(connectionId: string): Thenable<{ [name: string]: string }>; diff --git a/src/sqltest/parts/connection/connectionManagementService.test.ts b/src/sqltest/parts/connection/connectionManagementService.test.ts index c8cd5e70e0..34b0277b74 100644 --- a/src/sqltest/parts/connection/connectionManagementService.test.ts +++ b/src/sqltest/parts/connection/connectionManagementService.test.ts @@ -151,6 +151,7 @@ suite('SQL ConnectionManagementService tests', () => { function createConnectionManagementService(): ConnectionManagementService { let connectionManagementService = new ConnectionManagementService( connectionStore.object, + undefined, connectionDialogService.object, undefined, // IServerGroupController undefined, // IInstantiationService diff --git a/src/sqltest/parts/query/editor/queryActions.test.ts b/src/sqltest/parts/query/editor/queryActions.test.ts index f42e0a94c7..4a864abf58 100644 --- a/src/sqltest/parts/query/editor/queryActions.test.ts +++ b/src/sqltest/parts/query/editor/queryActions.test.ts @@ -126,7 +126,7 @@ suite('SQL QueryAction Tests', () => { .returns(() => Promise.resolve(none)); // ... Mock "isConnected" in ConnectionManagementService - let connectionManagementService = TypeMoq.Mock.ofType(ConnectionManagementService, TypeMoq.MockBehavior.Loose, {}, connectionDialogService.object); + let connectionManagementService = TypeMoq.Mock.ofType(ConnectionManagementService, TypeMoq.MockBehavior.Loose, {}, undefined, connectionDialogService.object); connectionManagementService.callBase = true; connectionManagementService.setup(x => x.isConnected(TypeMoq.It.isAnyString())).returns(() => isConnected); @@ -260,7 +260,7 @@ suite('SQL QueryAction Tests', () => { }); // ... Mock "isConnected" in ConnectionManagementService - let connectionManagementService = TypeMoq.Mock.ofType(ConnectionManagementService, TypeMoq.MockBehavior.Loose, {}, connectionDialogService.object); + let connectionManagementService = TypeMoq.Mock.ofType(ConnectionManagementService, TypeMoq.MockBehavior.Loose, {}, undefined, connectionDialogService.object); connectionManagementService.callBase = true; connectionManagementService.setup(x => x.isConnected(TypeMoq.It.isAnyString())).returns(() => isConnected); @@ -396,7 +396,7 @@ suite('SQL QueryAction Tests', () => { .returns(() => Promise.resolve(none)); // ... Mock "isConnected" in ConnectionManagementService - let connectionManagementService = TypeMoq.Mock.ofType(ConnectionManagementService, TypeMoq.MockBehavior.Loose, {}, connectionDialogService.object); + let connectionManagementService = TypeMoq.Mock.ofType(ConnectionManagementService, TypeMoq.MockBehavior.Loose, {}, undefined, connectionDialogService.object); connectionManagementService.callBase = true; connectionManagementService.setup(x => x.isConnected(TypeMoq.It.isAnyString())).returns(() => isConnected); @@ -442,7 +442,7 @@ suite('SQL QueryAction Tests', () => { .returns(() => Promise.resolve(none)); // ... Mock "isConnected" in ConnectionManagementService - let connectionManagementService = TypeMoq.Mock.ofType(ConnectionManagementService, TypeMoq.MockBehavior.Loose, {}, connectionDialogService.object); + let connectionManagementService = TypeMoq.Mock.ofType(ConnectionManagementService, TypeMoq.MockBehavior.Loose, {}, undefined, connectionDialogService.object); connectionManagementService.callBase = true; connectionManagementService.setup(x => x.isConnected(TypeMoq.It.isAnyString())).returns(() => isConnected); diff --git a/src/sqltest/services/connectionManagement/connectionManagementService.test.ts b/src/sqltest/services/connectionManagement/connectionManagementService.test.ts new file mode 100644 index 0000000000..c668de355c --- /dev/null +++ b/src/sqltest/services/connectionManagement/connectionManagementService.test.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as TypeMoq from 'typemoq'; +import { ConnectionManagementService } from 'sql/platform/connection/common/connectionManagementService'; +import { ConnectionStatusManager } from 'sql/platform/connection/common/connectionStatusManager'; +import { ConnectionStore } from 'sql/platform/connection/common/connectionStore'; +import { TestStorageService } from 'vs/workbench/test/workbenchTestServices'; +import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; +import { CapabilitiesTestService } from 'sqltest/stubs/capabilitiesTestService'; +import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup'; +import { integrated, mssqlProviderName } from 'sql/platform/connection/common/constants'; + +const capabilitiesService = new CapabilitiesTestService(); + +suite('ConnectionManagementService Tests:', () => { + test('getConnections test', () => { + const connectionStatusManagerMock = TypeMoq.Mock.ofType(ConnectionStatusManager, TypeMoq.MockBehavior.Loose); + const connectionStoreMock = TypeMoq.Mock.ofType(ConnectionStore, TypeMoq.MockBehavior.Loose, new TestStorageService()); + + connectionStatusManagerMock.setup(x => x.getActiveConnectionProfiles(undefined)).returns(() => { + return [createConnectionProfile('1'), createConnectionProfile('2')]; + }); + connectionStoreMock.setup(x => x.getRecentlyUsedConnections(undefined)).returns(() => { + return [createConnectionProfile('1'), createConnectionProfile('3')]; + }); + + const group1 = createConnectionGroup('group1'); + const group2 = createConnectionGroup('group2'); + group1.connections = [createConnectionProfile('1'), createConnectionProfile('4')]; + group1.children = [group2]; + group2.connections = [createConnectionProfile('5'), createConnectionProfile('6')]; + connectionStoreMock.setup(x => x.getConnectionProfileGroups(TypeMoq.It.isAny(), undefined)).returns(() => { + return [group1]; + }); + const connectionManagementService = new ConnectionManagementService(connectionStoreMock.object, connectionStatusManagerMock.object, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined); + + // dupe connections have been seeded the numbers below already reflected the de-duped results + + const verifyConnections = (actualConnections: ConnectionProfile[], expectedConnectionIds: string[], scenario: string) => { + assert.equal(actualConnections.length, expectedConnectionIds.length, 'incorrect number of connections returned, ' + scenario); + assert.deepEqual(actualConnections.map(conn => conn.id).sort(), expectedConnectionIds.sort(), 'connections do not match expectation, ' + scenario); + }; + + // no parameter - default to false + let connections = connectionManagementService.getConnections(); + verifyConnections(connections, ['1', '2', '3', '4', '5', '6'], 'no parameter provided'); + + // explicitly set to false + connections = connectionManagementService.getConnections(false); + verifyConnections(connections, ['1', '2', '3', '4', '5', '6'], 'parameter is false'); + + // active connections only + connections = connectionManagementService.getConnections(true); + verifyConnections(connections, ['1', '2'], 'parameter is true'); + }); +}); + +function createConnectionProfile(id: string): ConnectionProfile { + + return new ConnectionProfile(capabilitiesService, { + connectionName: 'newName', + savePassword: false, + groupFullName: 'testGroup', + serverName: 'testServerName', + databaseName: 'testDatabaseName', + authenticationType: integrated, + password: 'test', + userName: 'testUsername', + groupId: undefined, + providerName: mssqlProviderName, + options: {}, + saveProfile: true, + id: id + }); +} + +function createConnectionGroup(id: string): ConnectionProfileGroup { + return new ConnectionProfileGroup(id, undefined, id, undefined, undefined); +} \ No newline at end of file diff --git a/src/sqltest/stubs/connectionManagementService.test.ts b/src/sqltest/stubs/connectionManagementService.test.ts index 2b1b39d95f..e7759fd903 100644 --- a/src/sqltest/stubs/connectionManagementService.test.ts +++ b/src/sqltest/stubs/connectionManagementService.test.ts @@ -291,4 +291,8 @@ export class TestConnectionManagementService implements IConnectionManagementSer getDefaultProviderId(): string { return undefined; } + + getConnections(activeConnectionsOnly?: boolean): ConnectionProfile[] { + return []; + } }