diff --git a/src/sql/platform/capabilities/test/common/testCapabilitiesService.ts b/src/sql/platform/capabilities/test/common/testCapabilitiesService.ts index e4195a4115..ed4ee7c801 100644 --- a/src/sql/platform/capabilities/test/common/testCapabilitiesService.ts +++ b/src/sql/platform/capabilities/test/common/testCapabilitiesService.ts @@ -97,10 +97,24 @@ export class TestCapabilitiesService implements ICapabilitiesService { valueType: ServiceOptionType.string } ]; + let mssqlAdvancedOptions: azdata.ConnectionOption[] = [ + { + name: 'trustServerCertificate', + displayName: undefined!, + description: undefined!, + groupName: undefined!, + categoryValues: undefined!, + defaultValue: 'false', + isIdentity: false, + isRequired: false, + specialValueType: undefined!, + valueType: ServiceOptionType.boolean + } + ]; let msSQLCapabilities = { providerId: mssqlProviderName, displayName: 'MSSQL', - connectionOptions: connectionProvider, + connectionOptions: connectionProvider.concat(mssqlAdvancedOptions), }; let pgSQLCapabilities = { providerId: this.pgsqlProviderName, diff --git a/src/sql/platform/connection/common/providerConnectionInfo.ts b/src/sql/platform/connection/common/providerConnectionInfo.ts index c12aa85a83..86be625720 100644 --- a/src/sql/platform/connection/common/providerConnectionInfo.ts +++ b/src/sql/platform/connection/common/providerConnectionInfo.ts @@ -202,6 +202,27 @@ export class ProviderConnectionInfo implements azdata.ConnectionInfo { * Example: "providerName:MSSQL|authenticationType:|databaseName:database|serverName:server3|userName:user|group:testid" */ public getOptionsKey(): string { + let idNames = this.getOptionKeyIdNames(); + idNames = idNames.filter(x => x !== undefined); + + //Sort to make sure using names in the same order every time otherwise the ids would be different + idNames.sort(); + + let idValues: string[] = []; + for (let index = 0; index < idNames.length; index++) { + let value = this.options[idNames[index]!]; + value = value ? value : ''; + idValues.push(`${idNames[index]}${ProviderConnectionInfo.nameValueSeparator}${value}`); + } + + return ProviderConnectionInfo.ProviderPropertyName + ProviderConnectionInfo.nameValueSeparator + + this.providerName + ProviderConnectionInfo.idSeparator + idValues.join(ProviderConnectionInfo.idSeparator); + } + + /** + * @returns Array of option key names + */ + public getOptionKeyIdNames(): string[] { let idNames = []; if (this.serverCapabilities) { idNames = this.serverCapabilities.connectionOptions.map(o => { @@ -217,21 +238,7 @@ export class ProviderConnectionInfo implements azdata.ConnectionInfo { // This should never happen but just incase the serverCapabilities was not ready at this time idNames = ['authenticationType', 'database', 'server', 'user']; } - - idNames = idNames.filter(x => x !== undefined); - - //Sort to make sure using names in the same order every time otherwise the ids would be different - idNames.sort(); - - let idValues: string[] = []; - for (let index = 0; index < idNames.length; index++) { - let value = this.options[idNames[index]!]; - value = value ? value : ''; - idValues.push(`${idNames[index]}${ProviderConnectionInfo.nameValueSeparator}${value}`); - } - - return ProviderConnectionInfo.ProviderPropertyName + ProviderConnectionInfo.nameValueSeparator + - this.providerName + ProviderConnectionInfo.idSeparator + idValues.join(ProviderConnectionInfo.idSeparator); + return idNames; } public static getProviderFromOptionsKey(optionsKey: string) { diff --git a/src/sql/workbench/contrib/commandLine/electron-browser/commandLine.ts b/src/sql/workbench/contrib/commandLine/electron-browser/commandLine.ts index 90c7d5a171..41e5d8d594 100644 --- a/src/sql/workbench/contrib/commandLine/electron-browser/commandLine.ts +++ b/src/sql/workbench/contrib/commandLine/electron-browser/commandLine.ts @@ -31,17 +31,61 @@ import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/envi import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; export interface SqlArgs { + /** + * Used to determine file paths to be opened with SQL Editor. + * If provided, we connect the given profile to to it. + * More than one files can be passed to connect to provided profile. + */ _?: string[]; + /** + * Provide authenticationType to be used. + * accepted values: AzureMFA, SqlLogin, Integrated, etc. + */ authenticationType?: string + /** + * Name of database + */ database?: string; + /** + * Name of server + */ server?: string; + /** + * User name/email address + */ user?: string; + /** + * Operation to perform: + * accepted values: connect, openConnectionDialog + */ command?: string; + /** + * Name of connection provider, + * accepted values: mssql (by default), pgsql, etc. + */ provider?: string; - aad?: boolean; // deprecated - used by SSMS - authenticationType should be used instead - integrated?: boolean; // deprecated - used by SSMS - authenticationType should be used instead. + /** + * Deprecated - used by SSMS - authenticationType should be used instead + */ + aad?: boolean; + /** + * Deprecated - used by SSMS - authenticationType should be used instead. + */ + integrated?: boolean; + /** + * Whether or not to show dashboard + * accepted values: true, false (by default). + */ showDashboard?: boolean; + /** + * Supports providing applicationName that will be used for connection profile app name. + */ applicationName?: string; + /** + * Supports providing advanced connection properties that providers support. + * Value must be a json object containing key-value pairs in format: '{"key1":"value1","key2":"value2",...}' + */ + connectionProperties?: string; } //#region decorators @@ -324,9 +368,33 @@ export class CommandLineWorkbenchContribution implements IWorkbenchContribution, profile.setOptionValue('applicationName', applicationName); profile.setOptionValue('databaseDisplayName', profile.databaseName); profile.setOptionValue('groupId', profile.groupId); + // Set all advanced options + let advancedOptions = this.getAdvancedOptions(args.connectionProperties, profile.getOptionKeyIdNames()); + advancedOptions.forEach((v, k) => { + profile.setOptionValue(k, v); + }); return this._connectionManagementService ? this.tryMatchSavedProfile(profile) : profile; } + private getAdvancedOptions(options: string, idNames: string[]): Map { + const ignoredProperties = idNames.concat(['password', 'azureAccountToken']); + let advancedOptionsMap = new Map(); + if (options) { + try { + // Decode options if they contain any encoded URL characters + options = decodeURI(options); + JSON.parse(options, (k, v) => { + if (!(k in ignoredProperties)) { + advancedOptionsMap.set(k, v); + } + }); + } catch (e) { + throw new Error(localize('commandline.propertiesFormatError', 'Advanced connection properties could not be parsed as JSON, error occurred: {0} Received properties value: {1}', e, options)); + } + } + return advancedOptionsMap; + } + private tryMatchSavedProfile(profile: ConnectionProfile) { let match: ConnectionProfile = undefined; // If we can find a saved mssql provider connection that matches the args, use it diff --git a/src/sql/workbench/contrib/commandLine/test/electron-browser/commandLine.test.ts b/src/sql/workbench/contrib/commandLine/test/electron-browser/commandLine.test.ts index 046629c6e7..41ebc82140 100644 --- a/src/sql/workbench/contrib/commandLine/test/electron-browser/commandLine.test.ts +++ b/src/sql/workbench/contrib/commandLine/test/electron-browser/commandLine.test.ts @@ -96,6 +96,7 @@ class TestParsedArgs implements NativeParsedArgs, SqlArgs { waitMarkerFilePath?: string; authenticationType?: string; applicationName?: string; + connectionProperties?: string; } suite('commandLineService tests', () => { @@ -219,6 +220,42 @@ suite('commandLineService tests', () => { connectionManagementService.verifyAll(); }); + test('processCommandLine loads advanced options in args', async () => { + const connectionManagementService: TypeMoq.Mock + = TypeMoq.Mock.ofType(TestConnectionManagementService, TypeMoq.MockBehavior.Strict); + + const args: TestParsedArgs = new TestParsedArgs(); + args.server = 'myserver'; + args.database = 'mydatabase'; + args.user = 'myuser'; + args.authenticationType = Constants.AuthenticationType.SqlLogin; + args.applicationName = 'myapplication'; + // Pass advanced connection properties + args.connectionProperties = `{"trustServerCertificate":"true"}`; + + connectionManagementService.setup((c) => c.showConnectionDialog()).verifiable(TypeMoq.Times.never()); + connectionManagementService.setup(c => c.hasRegisteredServers()).returns(() => true).verifiable(TypeMoq.Times.atMostOnce()); + connectionManagementService.setup(c => c.getConnectionGroups(TypeMoq.It.isAny())).returns(() => []); + let originalProfile: IConnectionProfile = undefined; + connectionManagementService.setup(c => c.connectIfNotConnected(TypeMoq.It.is( + p => p.serverName === 'myserver' + && p.authenticationType === Constants.AuthenticationType.SqlLogin + && p.options['applicationName'] === 'myapplication-azdata'), 'connection', true)) + .returns((conn) => { + originalProfile = conn; + return Promise.resolve('unused'); + }) + .verifiable(TypeMoq.Times.once()); + connectionManagementService.setup(c => c.getConnectionProfileById(TypeMoq.It.isAnyString())).returns(() => originalProfile); + const configurationService = getConfigurationServiceMock(true); + const logService = new NullLogService(); + let contribution = getCommandLineContribution(connectionManagementService.object, configurationService.object, capabilitiesService, undefined, undefined, logService); + await contribution.processCommandLine(args); + assert.equal(originalProfile.options['applicationName'], 'myapplication-azdata', 'Application Name not received as expected.'); + assert.equal(originalProfile.options['trustServerCertificate'], 'true', 'Advanced option not received as expected.'); + connectionManagementService.verifyAll(); + }); + test('processCommandLine invokes a command without a profile parameter when no server is passed', async () => { const connectionManagementService: TypeMoq.Mock = TypeMoq.Mock.ofType(TestConnectionManagementService, TypeMoq.MockBehavior.Loose); @@ -568,7 +605,5 @@ suite('commandLineService tests', () => { notificationService.verifyAll(); connectionManagementService.verifyAll(); }); - - }); });