diff --git a/extensions/sql-bindings/src/common/azureFunctionsUtils.ts b/extensions/sql-bindings/src/common/azureFunctionsUtils.ts index a5a713ef6c..0304f025b2 100644 --- a/extensions/sql-bindings/src/common/azureFunctionsUtils.ts +++ b/extensions/sql-bindings/src/common/azureFunctionsUtils.ts @@ -189,21 +189,21 @@ export async function getHostFiles(): Promise { /** * Gets the local.settings.json file path - * @param projectFile path of the azure function project + * @param projectFolder The path to the project the setting should be added to * @returns the local.settings.json file path */ -export async function getSettingsFile(projectFile: string): Promise { - return path.join(path.dirname(projectFile), 'local.settings.json'); +export async function getSettingsFile(projectFolder: string): Promise { + return path.join(projectFolder, 'local.settings.json'); } /** - * Retrieves the new function file once the file is created and the watcher disposable - * @param projectFile is the path to the project file + * New azure function file watcher and watcher disposable to be used to watch for changes to the azure function project + * @param projectFolder is the parent directory to the project file * @returns the function file path once created and the watcher disposable */ -export function waitForNewFunctionFile(projectFile: string): IFileFunctionObject { +export function waitForNewFunctionFile(projectFolder: string): IFileFunctionObject { const watcher = vscode.workspace.createFileSystemWatcher(( - path.dirname(projectFile), '**/*.cs'), false, true, true); + new vscode.RelativePattern(projectFolder, '**/*.cs')), false, true, true); const filePromise = new Promise((resolve, _) => { watcher.onDidCreate((e) => { resolve(e.fsPath); @@ -243,12 +243,13 @@ export async function addNugetReferenceToProjectFile(selectedProjectFile: string /** * Adds the Sql Connection String to the local.settings.json * @param connectionString of the SQL Server connection that was chosen by the user - * @param projectFile The path to the project the setting should be added to + * @param projectFolder The path to the project the setting should be added to + * @param settingName The name of the setting to add to the local.settings.json */ -export async function addConnectionStringToConfig(connectionString: string, projectFile: string): Promise { - const settingsFile = await getSettingsFile(projectFile); +export async function addConnectionStringToConfig(connectionString: string, projectFolder: string, settingName: string = constants.sqlConnectionStringSetting): Promise { + const settingsFile = await getSettingsFile(projectFolder); if (settingsFile) { - await setLocalAppSetting(path.dirname(settingsFile), constants.sqlConnectionStringSetting, connectionString); + await setLocalAppSetting(path.dirname(settingsFile), settingName, connectionString); } } @@ -426,7 +427,6 @@ export async function promptAndUpdateConnectionStringSetting(projectUri: vscode. continue; } - // show the connection string methods (user input and connection profile options) const listOfConnectionStringMethods = [constants.connectionProfile, constants.userConnectionString]; let selectedConnectionStringMethod: string | undefined; let connectionString: string | undefined = ''; @@ -560,7 +560,7 @@ export async function promptConnectionStringPasswordAndUpdateConnectionString(co // 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 + // open local.settings.json file (if it exists) void vscode.commands.executeCommand(constants.vscodeOpenCommand, vscode.Uri.file(localSettingsPath)); } }); @@ -575,3 +575,21 @@ export async function promptConnectionStringPasswordAndUpdateConnectionString(co return undefined; } } + +export async function promptSelectDatabase(connectionInfo: IConnectionInfo): Promise { + const vscodeMssqlApi = await utils.getVscodeMssqlApi(); + + let connectionURI = await vscodeMssqlApi.connect(connectionInfo); + let listDatabases = await vscodeMssqlApi.listDatabases(connectionURI); + const selectedDatabase = (await vscode.window.showQuickPick(listDatabases, { + canPickMany: false, + title: constants.selectDatabase, + ignoreFocusOut: true + })); + + if (!selectedDatabase) { + // User cancelled + return undefined; + } + return selectedDatabase; +} diff --git a/extensions/sql-bindings/src/common/constants.ts b/extensions/sql-bindings/src/common/constants.ts index a5ab61200b..af737dde9a 100644 --- a/extensions/sql-bindings/src/common/constants.ts +++ b/extensions/sql-bindings/src/common/constants.ts @@ -32,6 +32,7 @@ export const timeoutProjectError = localize('timeoutProjectError', 'Timed out wa export function errorNewAzureFunction(error: any): string { return localize('errorNewAzureFunction', 'Error creating new Azure Function: {0}', utils.getErrorMessage(error)); } 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 workspaceMustBeUsed = localize('workspaceMustBeUsed', 'The current folder is not a workspace folder. Please open a workspace folder and try again.'); export const needConnection = localize('needConnection', 'A connection is required to use Azure Function with SQL Binding'); export const selectDatabase = localize('selectDatabase', 'Select Database'); export const browseEllipsisWithIcon = `$(folder) ${localize('browseEllipsis', "Browse...")}`; diff --git a/extensions/sql-bindings/src/common/telemetry.ts b/extensions/sql-bindings/src/common/telemetry.ts index 57993126dd..e142459bd6 100644 --- a/extensions/sql-bindings/src/common/telemetry.ts +++ b/extensions/sql-bindings/src/common/telemetry.ts @@ -17,8 +17,6 @@ export enum TelemetryViews { export enum TelemetryActions { // Create Azure Function with Sql Binding from Table startCreateAzureFunctionWithSqlBinding = 'startCreateAzureFunctionWithSqlBinding', - helpCreateAzureFunctionProject = 'helpCreateAzureFunctionProject', - learnMore = 'learnMore', finishCreateAzureFunctionWithSqlBinding = 'finishCreateAzureFunctionWithSqlBinding', exitCreateAzureFunctionQuickpick = 'exitCreateAzureFunctionQuickpick', @@ -31,3 +29,30 @@ export enum TelemetryActions { finishAddSqlBinding = 'finishAddSqlBinding', exitSqlBindingsQuickpick = 'exitSqlBindingsQuickpick', } + +export enum CreateAzureFunctionStep { + getAzureFunctionProject = 'getAzureFunctionProject', + learnMore = 'learnMore', + helpCreateAzureFunctionProject = 'helpCreateAzureFunctionProject', + getSelectedFolder = 'getSelectedFolder', + getBindingType = 'getBindingType', + launchFromCommandPalette = 'launchFromCommandPalette', + launchFromTable = 'launchFromTable', + getConnectionProfile = 'getConnectionProfile', + getDatabase = 'getDatabase', + getObjectName = 'getObjectName', + getConnectionString = 'getConnectionString', + getAzureFunctionName = 'getAzureFunctionName', + getTemplateId = 'getTemplateId', + setAzureWebJobsStorage = 'setAzureWebJobsStorage', + getConnectionStringSettingName = 'getConnectionStringSettingName', + promptForIncludePassword = 'promptForIncludePassword', +} + +export enum ExitReason { + cancelled = 'cancelled', + finishCreate = 'finishCreate', + timeout = 'timeout', + error = 'error', + exit = 'exit' +} \ No newline at end of file diff --git a/extensions/sql-bindings/src/common/utils.ts b/extensions/sql-bindings/src/common/utils.ts index c111836a35..1b0478d8bf 100644 --- a/extensions/sql-bindings/src/common/utils.ts +++ b/extensions/sql-bindings/src/common/utils.ts @@ -123,11 +123,16 @@ export function timeoutPromise(errorMessage: string, ms: number = 10000): Promis * Gets a unique file name * Increment the file name by adding 1 to function name if the file already exists * Undefined if the filename suffix count becomes greater than 1024 - * @param folderPath selected project folder path * @param fileName base filename to use + * @param folderPath selected project folder path * @returns a promise with the unique file name, or undefined */ -export async function getUniqueFileName(folderPath: string, fileName: string): Promise { +export async function getUniqueFileName(fileName: string, folderPath?: string): Promise { + if (!folderPath) { + // user is creating a brand new azure function project + return undefined; + } + let count: number = 0; const maxCount: number = 1024; let uniqueFileName = fileName; diff --git a/extensions/sql-bindings/src/services/azureFunctionsService.ts b/extensions/sql-bindings/src/services/azureFunctionsService.ts index 000aa596e2..46adf8a0fa 100644 --- a/extensions/sql-bindings/src/services/azureFunctionsService.ts +++ b/extensions/sql-bindings/src/services/azureFunctionsService.ts @@ -10,7 +10,7 @@ import * as utils from '../common/utils'; import * as azureFunctionsUtils from '../common/azureFunctionsUtils'; import * as constants from '../common/constants'; import * as azureFunctionsContracts from '../contracts/azureFunctions/azureFunctionsContracts'; -import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry'; +import { CreateAzureFunctionStep, TelemetryActions, TelemetryReporter, TelemetryViews, ExitReason } from '../common/telemetry'; import { AddSqlBindingParams, BindingType, GetAzureFunctionsParams, GetAzureFunctionsResult, ResultStatus } from 'sql-bindings'; import { IConnectionInfo, ITreeNodeInfo } from 'vscode-mssql'; @@ -20,285 +20,251 @@ 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'; + let telemetryStep: string = ''; + let exitReason: string = ExitReason.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; - propertyBag.bindingType = selectedBindingType; - TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.startCreateAzureFunctionWithSqlBinding) + let isCreateNewProject: boolean = false; + let newFunctionFileObject: azureFunctionsUtils.IFileFunctionObject | undefined; + + try { + const azureFunctionApi = await azureFunctionsUtils.getAzureFunctionsExtensionApi(); + if (!azureFunctionApi) { + exitReason = ExitReason.error; + propertyBag.exitReason = exitReason; + TelemetryReporter.createErrorEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.exitCreateAzureFunctionQuickpick) .withAdditionalProperties(propertyBag).send(); + return; + } + /** + * Prompts user for azure function project path to use + * If multiple found in workspace we prompt user to pick one + * If one found in workspace we use that + * If none found in workspace we show error message but continue with createFunction message + */ + let projectFile = await azureFunctionsUtils.getAzureFunctionProject(); + let projectFolder: string; + telemetryStep = CreateAzureFunctionStep.getAzureFunctionProject; + if (!projectFile) { + while (true) { + // show warning message that user needs an azure function project to create a function + let projectCreate = await vscode.window.showErrorMessage(constants.azureFunctionsProjectMustBeOpened, + constants.createProject, constants.learnMore); + if (projectCreate === constants.learnMore) { + telemetryStep = CreateAzureFunctionStep.learnMore; + exitReason = ExitReason.exit; + void vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(constants.sqlBindingsDoc)); + return; + } else if (projectCreate === constants.createProject) { + telemetryStep = CreateAzureFunctionStep.helpCreateAzureFunctionProject; + TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep) + .withAdditionalProperties(propertyBag).send(); + + isCreateNewProject = true; + telemetryStep = CreateAzureFunctionStep.getSelectedFolder; + // user either has not folder open or an empty workspace + // prompt user to choose a folder to create the project in + const browseProjectLocation = await vscode.window.showQuickPick( + [constants.browseEllipsisWithIcon], + { title: constants.selectAzureFunctionProjFolder, ignoreFocusOut: true }); + if (!browseProjectLocation) { + // User cancelled + return; + } + const projectFolders = (await vscode.window.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + openLabel: constants.selectButton + })); + if (!projectFolders) { + // User cancelled + return; + } + projectFolder = projectFolders[0].fsPath; + TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep) + .withAdditionalProperties(propertyBag).send(); + break; + } + } + } else { + // user has an azure function project open + projectFolder = path.dirname(projectFile); + } + // create a system file watcher for the project folder + newFunctionFileObject = azureFunctionsUtils.waitForNewFunctionFile(projectFolder); + + // Prompt user for binding type + telemetryStep = CreateAzureFunctionStep.getBindingType; + let selectedBindingType: BindingType | undefined; + let selectedBinding = await azureFunctionsUtils.promptForBindingType(); + if (!selectedBinding) { + return; + } + selectedBindingType = selectedBinding; + propertyBag.bindingType = selectedBindingType; + TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep) + .withAdditionalProperties(propertyBag).send(); + + + // Get connection string parameters and construct object name from prompt or connectionInfo given + let objectName: string | undefined; + const vscodeMssqlApi = await utils.getVscodeMssqlApi(); + if (!node) { + // if user selects command in command palette we prompt user for information + telemetryStep = CreateAzureFunctionStep.launchFromCommandPalette; // prompt user for connection profile to get connection info - quickPickStep = 'getConnectionInfo'; + telemetryStep = CreateAzureFunctionStep.getConnectionProfile; connectionInfo = await vscodeMssqlApi.promptForConnection(true); if (!connectionInfo) { // User cancelled return; } - TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.startCreateAzureFunctionWithSqlBinding) + TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep) .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 - })); - + telemetryStep = CreateAzureFunctionStep.getDatabase; + let selectedDatabase = await azureFunctionsUtils.promptSelectDatabase(connectionInfo); if (!selectedDatabase) { // User cancelled - return; + return undefined; } connectionInfo.database = selectedDatabase; // prompt user for object name to create function from + telemetryStep = CreateAzureFunctionStep.getObjectName; objectName = await azureFunctionsUtils.promptForObjectName(selectedBinding); if (!objectName) { // user cancelled return; } - - } catch (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; + } else { + // if user selects table in tree view we use connection info from Object Explorer node + telemetryStep = CreateAzureFunctionStep.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; + } } + objectName = utils.generateQuotedFullName(node.metadata.schema, node.metadata.name); + TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep) + .withAdditionalProperties(propertyBag).withConnectionInfo(connectionInfo).send(); } - // Ask binding type for promptObjectName - quickPickStep = 'getBindingType'; - let selectedBinding = await azureFunctionsUtils.promptForBindingType(); - if (!selectedBinding) { - // User cancelled + // get function name from user + telemetryStep = CreateAzureFunctionStep.getAzureFunctionName; + let functionName: string; + // remove special characters from function name + let uniqueObjectName = utils.santizeObjectName(objectName); + let uniqueFunctionName = await utils.getUniqueFileName(uniqueObjectName, projectFolder); + functionName = await vscode.window.showInputBox({ + title: constants.functionNameTitle, + value: uniqueFunctionName, + ignoreFocusOut: true, + validateInput: input => utils.validateFunctionName(input) + }) as string; + if (!functionName) { return; } - selectedBindingType = selectedBinding; - propertyBag.bindingType = selectedBinding; - TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.startCreateAzureFunctionWithSqlBinding) - .withAdditionalProperties(propertyBag).withConnectionInfo(connectionInfo).send(); + TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep) + .withAdditionalProperties(propertyBag) + .withConnectionInfo(connectionInfo).send(); - objectName = utils.generateQuotedFullName(node.metadata.schema, node.metadata.name); - } + // set the templateId based on the selected binding type + telemetryStep = CreateAzureFunctionStep.getTemplateId; + let templateId: string = selectedBindingType === BindingType.input ? constants.inputTemplateID : constants.outputTemplateID; - TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.startCreateAzureFunctionWithSqlBinding) - .withConnectionInfo(connectionInfo).send(); - quickPickStep = 'getAzureFunctionsExtensionApi'; - const azureFunctionApi = await azureFunctionsUtils.getAzureFunctionsExtensionApi(); - if (!azureFunctionApi) { - propertyBag.exitReason = exitReason; - TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.exitCreateAzureFunctionQuickpick) - .withConnectionInfo(connectionInfo) + // We need to set the azureWebJobsStorage to a placeholder + // to suppress the warning for opening the wizard - but will ask them to overwrite if they are creating new azureFunction + // issue https://github.com/microsoft/azuredatastudio/issues/18780 + telemetryStep = CreateAzureFunctionStep.setAzureWebJobsStorage; + await azureFunctionsUtils.setLocalAppSetting(projectFolder, constants.azureWebJobsStorageSetting, constants.azureWebJobsStoragePlaceholder); + + // prompt for Connection String Setting Name + let connectionStringSettingName: string | undefined = constants.sqlConnectionStringSetting; + if (!isCreateNewProject && projectFile) { + telemetryStep = CreateAzureFunctionStep.getConnectionStringSettingName; + connectionStringSettingName = await azureFunctionsUtils.promptAndUpdateConnectionStringSetting(vscode.Uri.parse(projectFile), connectionInfo); + TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep) + .withAdditionalProperties(propertyBag) + .withConnectionInfo(connectionInfo).send(); + } + + // create C# Azure Function with SQL Binding + telemetryStep = 'createFunctionAPI'; + await azureFunctionApi.createFunction({ + language: 'C#', + targetFramework: 'netcoreapp3.1', + version: '~3', + templateId: templateId, + functionName: functionName, + functionSettings: { + connectionStringSetting: connectionStringSettingName, + ...(selectedBindingType === BindingType.input && { object: objectName }), + ...(selectedBindingType === BindingType.output && { table: objectName }) + }, + folderPath: projectFolder, + suppressCreateProjectPrompt: true + }); + TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep) + .withAdditionalProperties(propertyBag) + .withConnectionInfo(connectionInfo).send(); + + // check for the new function file to be created and dispose of the file system watcher + const timeoutForFunctionFile = utils.timeoutPromise(constants.timeoutAzureFunctionFileError); + let functionFilePath = await Promise.race([newFunctionFileObject.filePromise, timeoutForFunctionFile]); + + // prompt user for include password for connection string + if (isCreateNewProject && functionFilePath) { + telemetryStep = CreateAzureFunctionStep.promptForIncludePassword; + let settingsFile = await azureFunctionsUtils.getSettingsFile(projectFolder); + if (!settingsFile) { + return; + } + let connectionString = await azureFunctionsUtils.promptConnectionStringPasswordAndUpdateConnectionString(connectionInfo, settingsFile); + if (!connectionString) { + return; + } + void azureFunctionsUtils.addConnectionStringToConfig(connectionString, projectFolder, connectionStringSettingName); + } + + propertyBag.telemetryStep = telemetryStep; + exitReason = ExitReason.finishCreate; + TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.finishCreateAzureFunctionWithSqlBinding) + .withAdditionalProperties(propertyBag) + .withConnectionInfo(connectionInfo).send(); + } catch (error) { + let errorType = utils.getErrorType(error); + propertyBag.telemetryStep = telemetryStep; + if (errorType === 'TimeoutError') { + // this error can be cause by many different scenarios including timeout or error occurred during createFunction + exitReason = ExitReason.timeout; + console.log('Timed out waiting for Azure Function project to be created. This may not necessarily be an error, for example if the user canceled out of the create flow.'); + } else { + // else an error would occur during the createFunction + exitReason = ExitReason.error; + void vscode.window.showErrorMessage(constants.errorNewAzureFunction(error)); + } + TelemetryReporter.createErrorEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.exitCreateAzureFunctionQuickpick, undefined, errorType) .withAdditionalProperties(propertyBag).send(); return; - - } - let projectFile = await azureFunctionsUtils.getAzureFunctionProject(); - let newHostProjectFile!: azureFunctionsUtils.IFileFunctionObject; - let hostFile: string; - - if (!projectFile) { - let projectCreate = await vscode.window.showErrorMessage(constants.azureFunctionsProjectMustBeOpened, - constants.createProject, constants.learnMore); - if (projectCreate === constants.learnMore) { - quickPickStep = 'learnMore'; - void vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(constants.sqlBindingsDoc)); - TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.learnMore) - .withConnectionInfo(connectionInfo) - .withAdditionalProperties(propertyBag).send(); - return; - } else if (projectCreate === constants.createProject) { - quickPickStep = 'helpCreateAzureFunctionProject'; - TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.helpCreateAzureFunctionProject) - .withConnectionInfo(connectionInfo) - .withAdditionalProperties(propertyBag).send(); - - // start the create azure function project flow - try { - // First prompt user for project location. We need to do this ourselves due to an issue - // in the AF extension : https://github.com/microsoft/vscode-azurefunctions/issues/3115 - const browseProjectLocation = await vscode.window.showQuickPick( - [constants.browseEllipsisWithIcon], - { title: constants.selectAzureFunctionProjFolder, ignoreFocusOut: true }); - if (!browseProjectLocation) { - // User cancelled - return undefined; - } - const projectFolders = (await vscode.window.showOpenDialog({ - canSelectFiles: false, - canSelectFolders: true, - canSelectMany: false, - openLabel: constants.selectButton - })); - if (!projectFolders) { - // User cancelled - return; - } - const templateId: string = selectedBindingType === BindingType.input ? constants.inputTemplateID : constants.outputTemplateID; - // because of an AF extension API issue, we have to get the newly created file by adding a watcher - // issue: https://github.com/microsoft/vscode-azurefunctions/issues/3052 - newHostProjectFile = azureFunctionsUtils.waitForNewHostFile(); - await azureFunctionApi.createFunction({ - language: 'C#', - targetFramework: 'netcoreapp3.1', - templateId: templateId, - suppressCreateProjectPrompt: true, - folderPath: projectFolders[0].fsPath, - functionSettings: { - ...(selectedBindingType === BindingType.input && { object: objectName }), - ...(selectedBindingType === BindingType.output && { table: objectName }) - }, - }); - const timeoutForHostFile = utils.timeoutPromise(constants.timeoutProjectError); - hostFile = await Promise.race([newHostProjectFile.filePromise, timeoutForHostFile]); - if (hostFile) { - // start the add sql binding flow - projectFile = await azureFunctionsUtils.getAzureFunctionProject(); - } - } catch (error) { - let errorType = utils.getErrorType(error); - propertyBag.quickPickStep = quickPickStep; - - if (errorType === 'TimeoutError') { - // this error can be cause by many different scenarios including timeout or error occurred during createFunction - exitReason = 'timeout'; - console.log('Timed out waiting for Azure Function project to be created. This may not necessarily be an error, for example if the user canceled out of the create flow.'); - } else { - // else an error would occur during the createFunction - exitReason = 'error'; - void vscode.window.showErrorMessage(constants.errorNewAzureFunction(error)); - } - TelemetryReporter.createErrorEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.exitCreateAzureFunctionQuickpick, undefined, errorType) - .withAdditionalProperties(propertyBag).send(); - return; - } finally { - propertyBag.exitReason = exitReason; - TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.exitCreateAzureFunctionQuickpick) - .withConnectionInfo(connectionInfo) - .withAdditionalProperties(propertyBag).send(); - newHostProjectFile.watcherDisposable.dispose(); - } - } - } - - if (projectFile) { - // because of an AF extension API issue, we have to get the newly created file by adding a watcher - // issue: https://github.com/microsoft/vscode-azurefunctions/issues/2908 - const newFunctionFileObject = azureFunctionsUtils.waitForNewFunctionFile(projectFile); - let functionName: string; - - try { - // get function name from user - quickPickStep = 'getAzureFunctionName'; - // 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 => utils.validateFunctionName(input) - }) as string; - if (!functionName) { - return; - } - TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.getAzureFunctionProject) - .withAdditionalProperties(propertyBag) - .withConnectionInfo(connectionInfo).send(); - - // set the templateId based on the selected binding type - 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); - - // prompt for connection string setting name and set connection string in local.settings.json - quickPickStep = 'getConnectionStringSettingName'; - let connectionStringSettingName = await azureFunctionsUtils.promptAndUpdateConnectionStringSetting(vscode.Uri.parse(projectFile), connectionInfo); - - // create C# Azure Function with SQL Binding - await azureFunctionApi.createFunction({ - language: 'C#', - templateId: templateId, - functionName: functionName, - targetFramework: 'netcoreapp3.1', - functionSettings: { - connectionStringSetting: connectionStringSettingName, - ...(selectedBindingType === BindingType.input && { object: objectName }), - ...(selectedBindingType === BindingType.output && { table: objectName }) - }, - folderPath: projectFile - }); - - // check for the new function file to be created and dispose of the file system watcher - const timeoutForFunctionFile = utils.timeoutPromise(constants.timeoutAzureFunctionFileError); - await Promise.race([newFunctionFileObject.filePromise, timeoutForFunctionFile]); - propertyBag.quickPickStep = quickPickStep; - exitReason = 'finishCreate'; - TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.finishCreateAzureFunctionWithSqlBinding) - .withAdditionalProperties(propertyBag) - .withConnectionInfo(connectionInfo).send(); - } catch (e) { - let errorType = utils.getErrorType(e); - propertyBag.quickPickStep = quickPickStep; - - if (errorType === 'TimeoutError') { - // this error can be cause by many different scenarios including timeout or error occurred during createFunction - exitReason = 'timeout'; - console.log('Timed out waiting for Azure Function project to be created. This may not necessarily be an error, for example if the user canceled out of the create flow.'); - } else { - // else an error would occur during the createFunction - exitReason = 'error'; - void vscode.window.showErrorMessage(utils.getErrorMessage(e)); - } - TelemetryReporter.createErrorEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.exitCreateAzureFunctionQuickpick, undefined, errorType) - .withAdditionalProperties(propertyBag).send(); - return; - } finally { - propertyBag.quickPickStep = quickPickStep; - propertyBag.exitReason = exitReason; - TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.exitCreateAzureFunctionQuickpick) - .withConnectionInfo(connectionInfo) - .withAdditionalProperties(propertyBag).send(); + } finally { + propertyBag.telemetryStep = telemetryStep; + propertyBag.exitReason = exitReason; + TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.exitCreateAzureFunctionQuickpick) + .withAdditionalProperties(propertyBag).send(); + if (newFunctionFileObject) { newFunctionFileObject.watcherDisposable.dispose(); } - } else { - TelemetryReporter.sendErrorEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.finishCreateAzureFunctionWithSqlBinding); } } diff --git a/extensions/sql-bindings/src/test/common/azureFunctionsUtils.test.ts b/extensions/sql-bindings/src/test/common/azureFunctionsUtils.test.ts index 3928b461af..c3b9ba0e1b 100644 --- a/extensions/sql-bindings/src/test/common/azureFunctionsUtils.test.ts +++ b/extensions/sql-bindings/src/test/common/azureFunctionsUtils.test.ts @@ -12,7 +12,6 @@ import * as azureFunctionsUtils from '../../common/azureFunctionsUtils'; const rootFolderPath = 'test'; const localSettingsPath: string = path.join(rootFolderPath, 'local.settings.json'); -const projectFilePath: string = path.join(rootFolderPath, 'projectFilePath.csproj'); describe('Tests to verify Azure Functions Utils functions', function (): void { @@ -55,7 +54,7 @@ describe('Tests to verify Azure Functions Utils functions', function (): void { }); it('Should get settings file given project file', async () => { - const settingsFile = await azureFunctionsUtils.getSettingsFile(projectFilePath); + const settingsFile = await azureFunctionsUtils.getSettingsFile(rootFolderPath); should(settingsFile).equals(localSettingsPath); }); @@ -68,7 +67,7 @@ describe('Tests to verify Azure Functions Utils functions', function (): void { const connectionString = 'testConnectionString'; let writeFileStub = sinon.stub(fs.promises, 'writeFile'); - await azureFunctionsUtils.addConnectionStringToConfig(connectionString, projectFilePath); + await azureFunctionsUtils.addConnectionStringToConfig(connectionString, rootFolderPath); should(writeFileStub.calledWithExactly(localSettingsPath, `{\n "IsEncrypted": false,\n "Values": {\n "test1": "test1",\n "test2": "test2",\n "test3": "test3",\n "SqlConnectionString": "testConnectionString"\n }\n}`)).equals(true); }); diff --git a/extensions/sql-bindings/src/test/dialog/addSqlBindingQuickpick.test.ts b/extensions/sql-bindings/src/test/dialog/addSqlBindingQuickpick.test.ts index 921cb33d4f..115b5b009b 100644 --- a/extensions/sql-bindings/src/test/dialog/addSqlBindingQuickpick.test.ts +++ b/extensions/sql-bindings/src/test/dialog/addSqlBindingQuickpick.test.ts @@ -106,7 +106,7 @@ describe('Add SQL Binding quick pick', () => { // give object name let inputBoxStub = sinon.stub(vscode.window, 'showInputBox').onFirstCall().resolves('dbo.table1'); - // select connection profile + // select connection string setting name quickpickStub.onThirdCall().resolves({ label: constants.createNewLocalAppSettingWithIcon }); // give connection string setting name