From 80a9d94648d9bd7fb505a3cb3d69fdd17e9c6df5 Mon Sep 17 00:00:00 2001 From: Barbara Valdez <34872381+barbaravaldez@users.noreply.github.com> Date: Thu, 17 Mar 2022 00:21:49 -0700 Subject: [PATCH] Update SQL Bindings API (#18748) * refactor addSqlBindingQuickpick so they can be called using sql-bindings api --- .../src/common/azureFunctionsUtils.ts | 219 +++++++++++++++++- .../src/dialogs/addSqlBindingQuickpick.ts | 201 +--------------- extensions/sql-bindings/src/extension.ts | 13 +- extensions/sql-bindings/src/sql-bindings.d.ts | 25 +- 4 files changed, 254 insertions(+), 204 deletions(-) diff --git a/extensions/sql-bindings/src/common/azureFunctionsUtils.ts b/extensions/sql-bindings/src/common/azureFunctionsUtils.ts index 526f6264d8..0f4ae00b0d 100644 --- a/extensions/sql-bindings/src/common/azureFunctionsUtils.ts +++ b/extensions/sql-bindings/src/common/azureFunctionsUtils.ts @@ -4,10 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import * as os from 'os'; import * as fs from 'fs'; -import * as path from 'path'; import * as vscode from 'vscode'; import * as utils from './utils'; import * as constants from './constants'; +import { BindingType } from 'sql-bindings'; +import * as path from 'path'; +import { ConnectionDetails, IConnectionInfo } from 'vscode-mssql'; // https://github.com/microsoft/vscode-azurefunctions/blob/main/src/vscode-azurefunctions.api.d.ts import { AzureFunctionsExtensionApi } from '../typings/vscode-azurefunctions.api'; // https://github.com/microsoft/vscode-azuretools/blob/main/ui/api.d.ts @@ -307,3 +309,218 @@ export async function getAFProjectContainingFile(fileUri: vscode.Uri): Promise { return fs.existsSync(path.join(folderPath, constants.hostFileName)); } + +/** + * Prompts the user to select type of binding and returns result + */ +export async function promptForBindingType(): Promise<(vscode.QuickPickItem & { type: BindingType }) | undefined> { + const inputOutputItems: (vscode.QuickPickItem & { type: BindingType })[] = [ + { + label: constants.input, + type: BindingType.input + }, + { + label: constants.output, + type: BindingType.output + } + ]; + + const selectedBinding = (await vscode.window.showQuickPick(inputOutputItems, { + canPickMany: false, + title: constants.selectBindingType, + ignoreFocusOut: true + })); + + return selectedBinding; +} + +/** + * Prompts the user to enter object name for the SQL query + * @param bindingType Type of SQL Binding + */ +export async function promptForObjectName(bindingType: BindingType): Promise { + return vscode.window.showInputBox({ + prompt: bindingType === BindingType.input ? constants.sqlTableOrViewToQuery : constants.sqlTableToUpsert, + placeHolder: constants.placeHolderObject, + validateInput: input => input ? undefined : constants.nameMustNotBeEmpty, + ignoreFocusOut: true + }); +} + +/** + * Prompts the user to enter connection setting and updates it from AF project + * @param projectUri Azure Function project uri + */ +export async function promptAndUpdateConnectionStringSetting(projectUri: vscode.Uri | undefined): Promise { + let connectionStringSettingName: string | undefined; + const vscodeMssqlApi = await utils.getVscodeMssqlApi(); + + // show the settings from project's local.settings.json if there's an AF functions project + if (projectUri) { + let settings; + try { + settings = await getLocalSettingsJson(path.join(path.dirname(projectUri.fsPath!), constants.azureFunctionLocalSettingsFileName)); + } catch (e) { + void vscode.window.showErrorMessage(utils.getErrorMessage(e)); + return; + } + + let existingSettings: (vscode.QuickPickItem)[] = []; + if (settings?.Values) { + existingSettings = Object.keys(settings.Values).map(setting => { + return { + label: setting + } as vscode.QuickPickItem; + }); + } + + existingSettings.unshift({ label: constants.createNewLocalAppSettingWithIcon }); + let sqlConnectionStringSettingExists = existingSettings.find(s => s.label === constants.sqlConnectionStringSetting); + + while (!connectionStringSettingName) { + const selectedSetting = await vscode.window.showQuickPick(existingSettings, { + canPickMany: false, + title: constants.selectSetting, + ignoreFocusOut: true + }); + if (!selectedSetting) { + // User cancelled + return; + } + + if (selectedSetting.label === constants.createNewLocalAppSettingWithIcon) { + const newConnectionStringSettingName = await vscode.window.showInputBox( + { + title: constants.enterConnectionStringSettingName, + ignoreFocusOut: true, + value: sqlConnectionStringSettingExists ? '' : constants.sqlConnectionStringSetting, + validateInput: input => input ? undefined : constants.nameMustNotBeEmpty + } + ) ?? ''; + + if (!newConnectionStringSettingName) { + // go back to select setting quickpick if user escapes from inputting the setting name in case they changed their mind + continue; + } + + // show the connection string methods (user input and connection profile options) + const listOfConnectionStringMethods = [constants.connectionProfile, constants.userConnectionString]; + while (true) { + const selectedConnectionStringMethod = await vscode.window.showQuickPick(listOfConnectionStringMethods, { + canPickMany: false, + title: constants.selectConnectionString, + ignoreFocusOut: true + }); + if (!selectedConnectionStringMethod) { + // User cancelled + return; + } + + let connectionString: string = ''; + let includePassword: string | undefined; + let connectionInfo: IConnectionInfo | undefined; + let connectionDetails: ConnectionDetails; + if (selectedConnectionStringMethod === constants.userConnectionString) { + // User chooses to enter connection string manually + connectionString = await vscode.window.showInputBox( + { + title: constants.enterConnectionString, + ignoreFocusOut: true, + value: 'Server=localhost;Initial Catalog={db_name};User ID=sa;Password={your_password};Persist Security Info=False', + validateInput: input => input ? undefined : constants.valueMustNotBeEmpty + } + ) ?? ''; + } else { + // Let user choose from existing connections to create connection string from + connectionInfo = await vscodeMssqlApi.promptForConnection(true); + if (!connectionInfo) { + // User cancelled return to selectedConnectionStringMethod prompt + continue; + } + connectionDetails = { options: connectionInfo }; + try { + // Prompt to include password in connection string if authentication type is SqlLogin and connection has password saved + if (connectionInfo.authenticationType === 'SqlLogin' && connectionInfo.password) { + includePassword = await vscode.window.showQuickPick([constants.yesString, constants.noString], { + title: constants.includePassword, + canPickMany: false, + ignoreFocusOut: true + }); + if (includePassword === constants.yesString) { + // set connection string to include password + connectionString = await vscodeMssqlApi.getConnectionString(connectionDetails, true, false); + } + } + // set connection string to not include the password if connection info does not include password, or user chooses to not include password, or authentication type is not sql login + if (includePassword !== constants.yesString) { + connectionString = await vscodeMssqlApi.getConnectionString(connectionDetails, false, false); + } + } catch (e) { + // failed to get connection string for selected connection and will go back to prompt for connection string methods + console.warn(e); + void vscode.window.showErrorMessage(constants.failedToGetConnectionString); + continue; + } + } + if (connectionString) { + try { + const projectFolder: string = path.dirname(projectUri.fsPath); + const localSettingsPath: string = path.join(projectFolder, constants.azureFunctionLocalSettingsFileName); + let userPassword: string | undefined; + // Ask user to enter password if auth type is sql login and password is not saved + if (connectionInfo?.authenticationType === 'SqlLogin' && !connectionInfo?.password) { + userPassword = await vscode.window.showInputBox({ + prompt: constants.enterPasswordPrompt, + placeHolder: constants.enterPasswordManually, + ignoreFocusOut: true, + password: true, + validateInput: input => input ? undefined : constants.valueMustNotBeEmpty + }); + if (userPassword) { + // if user enters password replace password placeholder with user entered password + connectionString = connectionString.replace(constants.passwordPlaceholder, userPassword); + } + } + if (includePassword !== constants.yesString && !userPassword && connectionInfo?.authenticationType === 'SqlLogin') { + // if user does not want to include password or user does not enter password, show warning message that they will have to enter it manually later in local.settings.json + void vscode.window.showWarningMessage(constants.userPasswordLater, constants.openFile, constants.closeButton).then(async (result) => { + if (result === constants.openFile) { + // open local.settings.json file + void vscode.commands.executeCommand(constants.vscodeOpenCommand, vscode.Uri.file(localSettingsPath)); + } + }); + } + const success = await setLocalAppSetting(projectFolder, newConnectionStringSettingName, connectionString); + if (success) { + // exit both loops and insert binding + connectionStringSettingName = newConnectionStringSettingName; + break; + } else { + void vscode.window.showErrorMessage(constants.selectConnectionError()); + } + } catch (e) { + // display error message and show select setting quickpick again + void vscode.window.showErrorMessage(constants.selectConnectionError(e)); + continue; + } + } + } + } else { + // If user cancels out of this or doesn't want to overwrite an existing setting + // just return them to the select setting quickpick in case they changed their mind + connectionStringSettingName = selectedSetting.label; + } + } + // Add sql extension package reference to project. If the reference is already there, it doesn't get added again + await addNugetReferenceToProjectFile(projectUri.fsPath); + } else { + // if no AF project was found or there's more than one AF functions project in the workspace, + // ask for the user to input the setting name + connectionStringSettingName = await vscode.window.showInputBox({ + prompt: constants.connectionStringSetting, + placeHolder: constants.connectionStringSettingPlaceholder, + ignoreFocusOut: true + }); + } + return connectionStringSettingName; +} diff --git a/extensions/sql-bindings/src/dialogs/addSqlBindingQuickpick.ts b/extensions/sql-bindings/src/dialogs/addSqlBindingQuickpick.ts index 760754f003..d513d61bae 100644 --- a/extensions/sql-bindings/src/dialogs/addSqlBindingQuickpick.ts +++ b/extensions/sql-bindings/src/dialogs/addSqlBindingQuickpick.ts @@ -4,19 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import * as path from 'path'; -import { ConnectionDetails, IConnectionInfo } from 'vscode-mssql'; import * as constants from '../common/constants'; import * as utils from '../common/utils'; import * as azureFunctionsUtils from '../common/azureFunctionsUtils'; import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry'; -import { BindingType } from 'sql-bindings'; export async function launchAddSqlBindingQuickpick(uri: vscode.Uri | undefined): Promise { TelemetryReporter.sendActionEvent(TelemetryViews.SqlBindingsQuickPick, TelemetryActions.startAddSqlBinding); - - const vscodeMssqlApi = await utils.getVscodeMssqlApi(); - if (!uri) { // this command only shows in the command palette when the active editor is a .cs file, so we can safely assume that's the scenario // when this is called without a uri @@ -62,34 +56,14 @@ export async function launchAddSqlBindingQuickpick(uri: vscode.Uri | undefined): } // 2. select input or output binding - const inputOutputItems: (vscode.QuickPickItem & { type: BindingType })[] = [ - { - label: constants.input, - type: BindingType.input - }, - { - label: constants.output, - type: BindingType.output - } - ]; - - const selectedBinding = (await vscode.window.showQuickPick(inputOutputItems, { - canPickMany: false, - title: constants.selectBindingType, - ignoreFocusOut: true - })); + const selectedBinding = await azureFunctionsUtils.promptForBindingType(); if (!selectedBinding) { return; } // 3. ask for object name for the binding - const objectName = await vscode.window.showInputBox({ - prompt: selectedBinding.type === BindingType.input ? constants.sqlTableOrViewToQuery : constants.sqlTableToUpsert, - placeHolder: constants.placeHolderObject, - validateInput: input => input ? undefined : constants.nameMustNotBeEmpty, - ignoreFocusOut: true - }); + const objectName = await azureFunctionsUtils.promptForObjectName(selectedBinding.type); if (!objectName) { return; @@ -103,176 +77,7 @@ export async function launchAddSqlBindingQuickpick(uri: vscode.Uri | undefined): // continue even if there's no AF project found. The binding should still be able to be added as long as there was an azure function found in the file earlier } - let connectionStringSettingName; - - // show the settings from project's local.settings.json if there's an AF functions project - if (projectUri) { - let settings; - try { - settings = await azureFunctionsUtils.getLocalSettingsJson(path.join(path.dirname(projectUri.fsPath!), constants.azureFunctionLocalSettingsFileName)); - } catch (e) { - void vscode.window.showErrorMessage(utils.getErrorMessage(e)); - return; - } - - let existingSettings: (vscode.QuickPickItem)[] = []; - if (settings?.Values) { - existingSettings = Object.keys(settings.Values).map(setting => { - return { - label: setting - } as vscode.QuickPickItem; - }); - } - - existingSettings.unshift({ label: constants.createNewLocalAppSettingWithIcon }); - let sqlConnectionStringSettingExists = existingSettings.find(s => s.label === constants.sqlConnectionStringSetting); - - while (!connectionStringSettingName) { - const selectedSetting = await vscode.window.showQuickPick(existingSettings, { - canPickMany: false, - title: constants.selectSetting, - ignoreFocusOut: true - }); - if (!selectedSetting) { - // User cancelled - return; - } - - if (selectedSetting.label === constants.createNewLocalAppSettingWithIcon) { - const newConnectionStringSettingName = await vscode.window.showInputBox( - { - title: constants.enterConnectionStringSettingName, - ignoreFocusOut: true, - value: sqlConnectionStringSettingExists ? '' : constants.sqlConnectionStringSetting, - validateInput: input => input ? undefined : constants.nameMustNotBeEmpty - } - ) ?? ''; - - if (!newConnectionStringSettingName) { - // go back to select setting quickpick if user escapes from inputting the setting name in case they changed their mind - continue; - } - - // show the connection string methods (user input and connection profile options) - const listOfConnectionStringMethods = [constants.connectionProfile, constants.userConnectionString]; - while (true) { - const selectedConnectionStringMethod = await vscode.window.showQuickPick(listOfConnectionStringMethods, { - canPickMany: false, - title: constants.selectConnectionString, - ignoreFocusOut: true - }); - if (!selectedConnectionStringMethod) { - // User cancelled - return; - } - - let connectionString: string = ''; - let includePassword: string | undefined; - let connectionInfo: IConnectionInfo | undefined; - let connectionDetails: ConnectionDetails; - if (selectedConnectionStringMethod === constants.userConnectionString) { - // User chooses to enter connection string manually - connectionString = await vscode.window.showInputBox( - { - title: constants.enterConnectionString, - ignoreFocusOut: true, - value: 'Server=localhost;Initial Catalog={db_name};User ID=sa;Password={your_password};Persist Security Info=False', - validateInput: input => input ? undefined : constants.valueMustNotBeEmpty - } - ) ?? ''; - } else { - // Let user choose from existing connections to create connection string from - connectionInfo = await vscodeMssqlApi.promptForConnection(true); - if (!connectionInfo) { - // User cancelled return to selectedConnectionStringMethod prompt - continue; - } - connectionDetails = { options: connectionInfo }; - try { - // Prompt to include password in connection string if authentication type is SqlLogin and connection has password saved - if (connectionInfo.authenticationType === 'SqlLogin' && connectionInfo.password) { - includePassword = await vscode.window.showQuickPick([constants.yesString, constants.noString], { - title: constants.includePassword, - canPickMany: false, - ignoreFocusOut: true - }); - if (includePassword === constants.yesString) { - // set connection string to include password - connectionString = await vscodeMssqlApi.getConnectionString(connectionDetails, true, false); - } - } - // set connection string to not include the password if connection info does not include password, or user chooses to not include password, or authentication type is not sql login - if (includePassword !== constants.yesString) { - connectionString = await vscodeMssqlApi.getConnectionString(connectionDetails, false, false); - } - } catch (e) { - // failed to get connection string for selected connection and will go back to prompt for connection string methods - console.warn(e); - void vscode.window.showErrorMessage(constants.failedToGetConnectionString); - continue; - } - } - if (connectionString) { - try { - const projectFolder: string = path.dirname(projectUri.fsPath); - const localSettingsPath: string = path.join(projectFolder, constants.azureFunctionLocalSettingsFileName); - let userPassword: string | undefined; - // Ask user to enter password if auth type is sql login and password is not saved - if (connectionInfo?.authenticationType === 'SqlLogin' && !connectionInfo?.password) { - userPassword = await vscode.window.showInputBox({ - prompt: constants.enterPasswordPrompt, - placeHolder: constants.enterPasswordManually, - ignoreFocusOut: true, - password: true, - validateInput: input => input ? undefined : constants.valueMustNotBeEmpty - }); - if (userPassword) { - // if user enters password replace password placeholder with user entered password - connectionString = connectionString.replace(constants.passwordPlaceholder, userPassword); - } - } - if (includePassword !== constants.yesString && !userPassword && connectionInfo?.authenticationType === 'SqlLogin') { - // if user does not want to include password or user does not enter password, show warning message that they will have to enter it manually later in local.settings.json - void vscode.window.showWarningMessage(constants.userPasswordLater, constants.openFile, constants.closeButton).then(async (result) => { - if (result === constants.openFile) { - // open local.settings.json file - void vscode.commands.executeCommand(constants.vscodeOpenCommand, vscode.Uri.file(localSettingsPath)); - } - }); - } - const success = await azureFunctionsUtils.setLocalAppSetting(projectFolder, newConnectionStringSettingName, connectionString); - if (success) { - // exit both loops and insert binding - connectionStringSettingName = newConnectionStringSettingName; - break; - } else { - void vscode.window.showErrorMessage(constants.selectConnectionError()); - } - } catch (e) { - // display error message and show select setting quickpick again - void vscode.window.showErrorMessage(constants.selectConnectionError(e)); - continue; - } - } - } - } else { - // If user cancels out of this or doesn't want to overwrite an existing setting - // just return them to the select setting quickpick in case they changed their mind - connectionStringSettingName = selectedSetting.label; - } - } - // Add sql extension package reference to project. If the reference is already there, it doesn't get added again - await azureFunctionsUtils.addNugetReferenceToProjectFile(projectUri.fsPath); - } else { - // if no AF project was found or there's more than one AF functions project in the workspace, - // ask for the user to input the setting name - connectionStringSettingName = await vscode.window.showInputBox({ - prompt: constants.connectionStringSetting, - placeHolder: constants.connectionStringSettingPlaceholder, - ignoreFocusOut: true - }); - } - + let connectionStringSettingName = await azureFunctionsUtils.promptAndUpdateConnectionStringSetting(projectUri); if (!connectionStringSettingName) { return; } diff --git a/extensions/sql-bindings/src/extension.ts b/extensions/sql-bindings/src/extension.ts index 9d9ea28aef..ba5a41ddb5 100644 --- a/extensions/sql-bindings/src/extension.ts +++ b/extensions/sql-bindings/src/extension.ts @@ -6,12 +6,12 @@ import * as vscode from 'vscode'; import { ITreeNodeInfo } from 'vscode-mssql'; import { IExtension, BindingType } from 'sql-bindings'; import { getAzdataApi, getVscodeMssqlApi } from './common/utils'; -import { launchAddSqlBindingQuickpick } from './dialogs/addSqlBindingQuickpick'; import { addSqlBinding, createAzureFunction, getAzureFunctions } from './services/azureFunctionsService'; +import { launchAddSqlBindingQuickpick } from './dialogs/addSqlBindingQuickpick'; +import { promptForBindingType, promptAndUpdateConnectionStringSetting, promptForObjectName } from './common/azureFunctionsUtils'; export async function activate(context: vscode.ExtensionContext): Promise { const vscodeMssqlApi = await getVscodeMssqlApi(); - void vscode.commands.executeCommand('setContext', 'azdataAvailable', !!getAzdataApi()); // register the add sql binding command context.subscriptions.push(vscode.commands.registerCommand('sqlBindings.addSqlBinding', async (uri: vscode.Uri | undefined) => { return launchAddSqlBindingQuickpick(uri); })); @@ -37,6 +37,15 @@ export async function activate(context: vscode.ExtensionContext): Promise { return addSqlBinding(bindingType, filePath, functionName, objectName, connectionStringSetting); }, + promptForBindingType: async () => { + return promptForBindingType(); + }, + promptForObjectName: async (bindingType: BindingType) => { + return promptForObjectName(bindingType); + }, + promptAndUpdateConnectionStringSetting: async (projectUri: vscode.Uri | undefined) => { + return promptAndUpdateConnectionStringSetting(projectUri); + }, getAzureFunctions: async (filePath: string) => { return getAzureFunctions(filePath); } diff --git a/extensions/sql-bindings/src/sql-bindings.d.ts b/extensions/sql-bindings/src/sql-bindings.d.ts index 5174bd17be..d298e0f171 100644 --- a/extensions/sql-bindings/src/sql-bindings.d.ts +++ b/extensions/sql-bindings/src/sql-bindings.d.ts @@ -6,6 +6,8 @@ declare module 'sql-bindings' { + import * as vscode from 'vscode'; + export const enum extension { name = 'Microsoft.sql-bindings', vsCodeName = 'ms-mssql.sql-bindings-vscode' @@ -25,12 +27,29 @@ declare module 'sql-bindings' { */ addSqlBinding(bindingType: BindingType, filePath: string, functionName: string, objectName: string, connectionStringSetting: string): Promise; + /** + * Prompts the user to select type of binding and returns result + */ + promptForBindingType(): Promise<(vscode.QuickPickItem & { type: BindingType }) | undefined>; + + /** + * Prompts the user to enter object name for the SQL query + * @param bindingType Type of SQL Binding + */ + promptForObjectName(bindingType: BindingType): Promise; + + /** + * Prompts the user to enter connection setting and updates it from AF project + * @param projectUri Azure Function project uri + */ + promptAndUpdateConnectionStringSetting(projectUri: vscode.Uri | undefined): Promise; + /** * Gets the names of the Azure Functions in the file * @param filePath Path of the file to get the Azure Functions * @returns array of names of Azure Functions in the file */ - getAzureFunctions(filePath: string): Promise; + getAzureFunctions(filePath: string): Promise; } /** @@ -82,7 +101,7 @@ declare module 'sql-bindings' { /** * Parameters for getting the names of the Azure Functions in a file */ - export interface GetAzureFunctionsParams { + export interface GetAzureFunctionsParams { /** * Absolute file path of file to get Azure Functions */ @@ -92,7 +111,7 @@ declare module 'sql-bindings' { /** * Result from a get Azure Functions request */ - export interface GetAzureFunctionsResult extends ResultStatus { + export interface GetAzureFunctionsResult extends ResultStatus { /** * Array of names of Azure Functions in the file */