From ed8d2f99273fce54c2098b5a72cd35554d7c7286 Mon Sep 17 00:00:00 2001 From: Vasu Bhog Date: Fri, 8 Apr 2022 10:28:45 -0700 Subject: [PATCH] Fix for user using command palette (#18948) * fix for user using command palette command * rework if a user uses the create azure function via the command * for now only show in vs code * move logic to azureFunctionService + address comments * fix command location * address comments * fix validateFunction --- extensions/sql-bindings/package.json | 10 +- .../sql-bindings/src/common/constants.ts | 3 + extensions/sql-bindings/src/common/utils.ts | 25 ++++ extensions/sql-bindings/src/extension.ts | 21 +-- .../src/services/azureFunctionsService.ts | 125 +++++++++++++++--- 5 files changed, 142 insertions(+), 42 deletions(-) diff --git a/extensions/sql-bindings/package.json b/extensions/sql-bindings/package.json index 594bab2525..7753f5e08a 100644 --- a/extensions/sql-bindings/package.json +++ b/extensions/sql-bindings/package.json @@ -40,8 +40,7 @@ { "command": "sqlBindings.createAzureFunction", "title": "%sqlBindings.createAzureFunction%", - "category": "MS SQL", - "when": "view == objectExplorer && viewItem == Table" + "category": "MS SQL" } ], "menus": { @@ -49,12 +48,17 @@ { "command": "sqlBindings.addSqlBinding", "when": "editorLangId == csharp && !azdataAvailable && resourceScheme != untitled" + }, + { + "command": "sqlBindings.createAzureFunction", + "when": "view == objectExplorer && viewItem == Table && !azdataAvailable", + "group": "zAzure_Function@1" } ], "view/item/context": [ { "command": "sqlBindings.createAzureFunction", - "when": "view == objectExplorer && viewItem == Table", + "when": "view == objectExplorer && viewItem == Table && !azdataAvailable", "group": "zAzure_Function@1" } ] diff --git a/extensions/sql-bindings/src/common/constants.ts b/extensions/sql-bindings/src/common/constants.ts index c6a7b99a98..fe61c43b3e 100644 --- a/extensions/sql-bindings/src/common/constants.ts +++ b/extensions/sql-bindings/src/common/constants.ts @@ -32,6 +32,8 @@ export const timeoutProjectError = localize('timeoutProjectError', 'Timed out wa export const errorNewAzureFunction = localize('errorNewAzureFunction', 'Error creating new Azure Function: {0}'); export const azureFunctionsExtensionNotInstalled = localize('azureFunctionsExtensionNotInstalled', 'Azure Functions extension must be installed in order to use this feature.'); export const azureFunctionsProjectMustBeOpened = localize('azureFunctionsProjectMustBeOpened', 'A C# Azure Functions project must be present in order to create a new Azure Function for this table.'); +export const needConnection = localize('needConnection', 'A connection is required to use Azure Function with SQL Binding'); +export const selectDatabase = localize('selectDatabase', 'Select Database'); // Insert SQL binding export const hostFileName = 'host.json'; @@ -42,6 +44,7 @@ export const azureFunctionLocalSettingsFileName = 'local.settings.json'; export const vscodeOpenCommand = 'vscode.open'; export const nameMustNotBeEmpty = localize('nameMustNotBeEmpty', "Name must not be empty"); +export const hasSpecialCharacters = localize('hasSpecialCharacters', "Name must not include special characters"); export const yesString = localize('yesString', "Yes"); export const noString = localize('noString', "No"); export const input = localize('input', "Input"); diff --git a/extensions/sql-bindings/src/common/utils.ts b/extensions/sql-bindings/src/common/utils.ts index 88834d926e..5c7cd9f2ce 100644 --- a/extensions/sql-bindings/src/common/utils.ts +++ b/extensions/sql-bindings/src/common/utils.ts @@ -10,6 +10,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as glob from 'fast-glob'; import * as cp from 'child_process'; +import * as constants from '../common/constants'; export interface ValidationResult { errorMessage: string; @@ -162,6 +163,30 @@ export function escapeClosingBrackets(str: string): string { return str.replace(']', ']]'); } +/** + * Removes all special characters from object name + * @param objectName can include brackets/periods and user entered special characters + * @returns the object name without any special characters + */ +export function santizeObjectName(objectName: string): string { + return objectName.replace(/[^a-zA-Z0-9 ]/g, ''); +} + +/** + * Check to see if the input from user entered is valid + * @param input from user input + * @returns returns error if the input is empty or has special characters, undefined if the input is valid + */ +export function validateFunctionName(input: string): string | undefined { + const specialChars = /[`!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~]/; + if (!input) { + return constants.nameMustNotBeEmpty; + } else if (specialChars.test(input)) { + return constants.hasSpecialCharacters; + } + return undefined; +} + /** * Gets the package info for the extension based on where the extension is installed * @returns the package info object diff --git a/extensions/sql-bindings/src/extension.ts b/extensions/sql-bindings/src/extension.ts index e6c7fb4a4c..f9cd68b439 100644 --- a/extensions/sql-bindings/src/extension.ts +++ b/extensions/sql-bindings/src/extension.ts @@ -5,33 +5,18 @@ import * as vscode from 'vscode'; import { ITreeNodeInfo } from 'vscode-mssql'; import { IExtension, BindingType } from 'sql-bindings'; -import { getAzdataApi, getVscodeMssqlApi } from './common/utils'; +import { getAzdataApi } from './common/utils'; 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); })); // Generate Azure Function command - context.subscriptions.push(vscode.commands.registerCommand('sqlBindings.createAzureFunction', async (node: ITreeNodeInfo) => { - let connectionInfo = node.connectionInfo; - // set the database containing the selected table so it can be used - // for the initial catalog property of the connection string - let newNode: ITreeNodeInfo = node; - while (newNode) { - if (newNode.nodeType === 'Database') { - connectionInfo.database = newNode.metadata.name; - break; - } else { - newNode = newNode.parentNode; - } - } - const connectionDetails = vscodeMssqlApi.createConnectionDetails(connectionInfo); - const connectionString = await vscodeMssqlApi.getConnectionString(connectionDetails, false, false); - await createAzureFunction(connectionString, node.metadata.schema, node.metadata.name, connectionInfo); + context.subscriptions.push(vscode.commands.registerCommand('sqlBindings.createAzureFunction', async (node?: ITreeNodeInfo) => { + return await createAzureFunction(node); })); return { addSqlBinding: async (bindingType: BindingType, filePath: string, functionName: string, objectName: string, connectionStringSetting: string) => { diff --git a/extensions/sql-bindings/src/services/azureFunctionsService.ts b/extensions/sql-bindings/src/services/azureFunctionsService.ts index cc86b36380..1bf8cab09a 100644 --- a/extensions/sql-bindings/src/services/azureFunctionsService.ts +++ b/extensions/sql-bindings/src/services/azureFunctionsService.ts @@ -12,15 +12,110 @@ import * as constants from '../common/constants'; import * as azureFunctionsContracts from '../contracts/azureFunctions/azureFunctionsContracts'; import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry'; import { AddSqlBindingParams, BindingType, GetAzureFunctionsParams, GetAzureFunctionsResult, ResultStatus } from 'sql-bindings'; -import { IConnectionInfo } from 'vscode-mssql'; +import { IConnectionInfo, ITreeNodeInfo } from 'vscode-mssql'; export const hostFileName: string = 'host.json'; -export async function createAzureFunction(connectionString: string, schema: string, table: string, connectionInfo: IConnectionInfo): Promise { +export async function createAzureFunction(node?: ITreeNodeInfo): Promise { + // telemetry properties for create azure function let sessionId: string = uuid.v4(); let propertyBag: { [key: string]: string } = { sessionId: sessionId }; let quickPickStep: string = ''; let exitReason: string = 'cancelled'; + TelemetryReporter.sendActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.startCreateAzureFunctionWithSqlBinding); + + let selectedBindingType: BindingType | undefined; + let connectionInfo: IConnectionInfo | undefined; + let connectionURI: string; + let listDatabases: string[] | undefined; + let objectName: string | undefined; + const vscodeMssqlApi = await utils.getVscodeMssqlApi(); + if (!node) { + // if user selects command in command palette we prompt user for information + quickPickStep = 'launchFromCommandPalette'; + try { + // Ask binding type for promptObjectName + quickPickStep = 'getBindingType'; + let selectedBinding = await azureFunctionsUtils.promptForBindingType(); + if (!selectedBinding) { + return; + } + selectedBindingType = selectedBinding.type; + propertyBag.bindingType = selectedBindingType; + TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.startCreateAzureFunctionWithSqlBinding) + .withAdditionalProperties(propertyBag).send(); + + // prompt user for connection profile to get connection info + quickPickStep = 'getConnectionInfo'; + connectionInfo = await vscodeMssqlApi.promptForConnection(true); + if (!connectionInfo) { + // User cancelled + return; + } + TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.startCreateAzureFunctionWithSqlBinding) + .withAdditionalProperties(propertyBag).withConnectionInfo(connectionInfo).send(); + + // list databases based on connection profile selected + connectionURI = await vscodeMssqlApi.connect(connectionInfo); + listDatabases = await vscodeMssqlApi.listDatabases(connectionURI); + const selectedDatabase = (await vscode.window.showQuickPick(listDatabases, { + canPickMany: false, + title: constants.selectDatabase, + ignoreFocusOut: true + })); + + if (!selectedDatabase) { + // User cancelled + return; + } + connectionInfo.database = selectedDatabase; + + // prompt user for object name to create function from + objectName = await azureFunctionsUtils.promptForObjectName(selectedBinding.type); + if (!objectName) { + // user cancelled + return; + } + + } catch (e) { + void vscode.window.showErrorMessage(utils.getErrorMessage(e)); + propertyBag.quickPickStep = quickPickStep; + exitReason = 'error'; + TelemetryReporter.createErrorEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.exitCreateAzureFunctionQuickpick, undefined, utils.getErrorType(e)) + .withAdditionalProperties(propertyBag).send(); + return; + } + } else { + quickPickStep = 'launchFromTable'; + connectionInfo = node.connectionInfo; + // set the database containing the selected table so it can be used + // for the initial catalog property of the connection string + let newNode: ITreeNodeInfo = node; + while (newNode) { + if (newNode.nodeType === 'Database') { + connectionInfo.database = newNode.metadata.name; + break; + } else { + newNode = newNode.parentNode; + } + } + // Ask binding type for promptObjectName + quickPickStep = 'getBindingType'; + let selectedBinding = await azureFunctionsUtils.promptForBindingType(); + + if (!selectedBinding) { + // User cancelled + return; + } + selectedBindingType = selectedBinding.type; + propertyBag.bindingType = selectedBinding.type; + TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.startCreateAzureFunctionWithSqlBinding) + .withAdditionalProperties(propertyBag).withConnectionInfo(connectionInfo).send(); + + objectName = utils.generateQuotedFullName(node.metadata.schema, node.metadata.name); + } + const connectionDetails = vscodeMssqlApi.createConnectionDetails(connectionInfo); + const connectionString = await vscodeMssqlApi.getConnectionString(connectionDetails, false, false); TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.startCreateAzureFunctionWithSqlBinding) .withConnectionInfo(connectionInfo).send(); @@ -101,12 +196,14 @@ export async function createAzureFunction(connectionString: string, schema: stri try { // get function name from user quickPickStep = 'getAzureFunctionName'; - let uniqueFunctionName = await utils.getUniqueFileName(path.dirname(projectFile), table); + // remove special characters from function name + let uniqueObjectName = utils.santizeObjectName(objectName); + let uniqueFunctionName = await utils.getUniqueFileName(path.dirname(projectFile), uniqueObjectName); functionName = await vscode.window.showInputBox({ title: constants.functionNameTitle, value: uniqueFunctionName, ignoreFocusOut: true, - validateInput: input => input ? undefined : constants.nameMustNotBeEmpty + validateInput: input => utils.validateFunctionName(input) }) as string; if (!functionName) { return; @@ -115,26 +212,12 @@ export async function createAzureFunction(connectionString: string, schema: stri .withAdditionalProperties(propertyBag) .withConnectionInfo(connectionInfo).send(); - // select input or output binding - quickPickStep = 'getBindingType'; - const selectedBinding = await azureFunctionsUtils.promptForBindingType(); - - if (!selectedBinding) { - return; - } - propertyBag.bindingType = selectedBinding.type; - TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.startCreateAzureFunctionWithSqlBinding) - .withAdditionalProperties(propertyBag) - .withConnectionInfo(connectionInfo).send(); - // set the templateId based on the selected binding type - let templateId: string = selectedBinding.type === BindingType.input ? constants.inputTemplateID : constants.outputTemplateID; - let objectName = utils.generateQuotedFullName(schema, table); + let templateId: string = selectedBindingType === BindingType.input ? constants.inputTemplateID : constants.outputTemplateID; // We need to set the azureWebJobsStorage to a placeholder // to suppress the warning for opening the wizard // issue https://github.com/microsoft/azuredatastudio/issues/18780 - await azureFunctionsUtils.setLocalAppSetting(path.dirname(projectFile), constants.azureWebJobsStorageSetting, constants.azureWebJobsStoragePlaceholder); // create C# Azure Function with SQL Binding @@ -144,8 +227,8 @@ export async function createAzureFunction(connectionString: string, schema: stri functionName: functionName, functionSettings: { connectionStringSetting: constants.sqlConnectionStringSetting, - ...(selectedBinding.type === BindingType.input && { object: objectName }), - ...(selectedBinding.type === BindingType.output && { table: objectName }) + ...(selectedBindingType === BindingType.input && { object: objectName }), + ...(selectedBindingType === BindingType.output && { table: objectName }) }, folderPath: projectFile });