Files
azuredatastudio/extensions/sql-bindings/src/services/azureFunctionsService.ts
Charles Gagnon 60c1c8a89f Update ads-extension-telemetry to 1.3.1 (#20724)
* Update ads-extension-telemetry to 1.3.0

* fix
2022-10-04 23:06:53 -07:00

339 lines
15 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as path from 'path';
import * as uuid from 'uuid';
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 { CreateAzureFunctionStep, TelemetryActions, TelemetryReporter, TelemetryViews, ExitReason } from '../common/telemetry';
import { AddSqlBindingParams, BindingType, GetAzureFunctionsParams, GetAzureFunctionsResult, IConnectionStringInfo, ObjectType, ResultStatus } from 'sql-bindings';
import { IConnectionInfo, ITreeNodeInfo } from 'vscode-mssql';
import { createAddConnectionStringStep } from '../createNewProject/addConnectionStringStep';
export const hostFileName: string = 'host.json';
export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
// telemetry properties for create azure function
let sessionId: string = uuid.v4();
let propertyBag: { [key: string]: string } = { sessionId: sessionId };
let telemetryStep: CreateAzureFunctionStep | undefined = undefined;
let exitReason: string = ExitReason.cancelled;
TelemetryReporter.sendActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.startCreateAzureFunctionWithSqlBinding);
let connectionInfo: IConnectionInfo | undefined;
let isCreateNewProject: boolean = false;
try {
// check to see if Azure Functions Extension is installed
const azureFunctionApi = await azureFunctionsUtils.getAzureFunctionsExtensionApi();
if (!azureFunctionApi) {
exitReason = ExitReason.error;
propertyBag.exitReason = exitReason;
telemetryStep = CreateAzureFunctionStep.noAzureFunctionsExtension;
TelemetryReporter.createErrorEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep)
.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 no 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
exitReason = ExitReason.cancelled;
return;
}
const projectFolders = (await vscode.window.showOpenDialog({
canSelectFiles: false,
canSelectFolders: true,
canSelectMany: false,
openLabel: constants.selectButton
}));
if (!projectFolders) {
// User cancelled
exitReason = ExitReason.cancelled;
return;
}
projectFolder = projectFolders[0].fsPath;
TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep)
.withAdditionalProperties(propertyBag).send();
break;
} else {
// user cancelled
exitReason = ExitReason.cancelled;
return;
}
}
} else {
// user has an azure function project open
projectFolder = path.dirname(projectFile);
}
// Get connection string parameters and construct object name from prompt or connectionInfo given
let objectName: string | undefined;
let selectedBindingType: BindingType | undefined;
if (!node) {
// user selects command in command palette we prompt user for information
telemetryStep = CreateAzureFunctionStep.launchFromCommandPalette;
let chosenObjectType = await azureFunctionsUtils.promptForObjectType();
if (!chosenObjectType) {
// User cancelled
exitReason = ExitReason.cancelled;
return;
}
// Prompt user for binding type
telemetryStep = CreateAzureFunctionStep.getBindingType;
selectedBindingType = await azureFunctionsUtils.promptForBindingType(chosenObjectType);
if (!selectedBindingType) {
// User cancelled
exitReason = ExitReason.cancelled;
return;
}
// send telemetry for chosen object type and binding type
propertyBag.objectType = chosenObjectType;
propertyBag.bindingType = selectedBindingType;
TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep)
.withAdditionalProperties(propertyBag).send();
// prompt user for connection profile to get connection info
while (true) {
try {
const vscodeMssqlApi = await utils.getVscodeMssqlApi();
connectionInfo = await vscodeMssqlApi.promptForConnection(true);
} catch (e) {
// user cancelled while creating connection profile
// show the connection profile selection prompt again
continue;
}
if (!connectionInfo) {
// User cancelled
exitReason = ExitReason.cancelled;
return;
}
TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep)
.withAdditionalProperties(propertyBag).withConnectionInfo(connectionInfo).send();
telemetryStep = CreateAzureFunctionStep.getObjectName;
// prompt user for object name to create function from
objectName = await azureFunctionsUtils.promptForObjectName(selectedBindingType, connectionInfo, chosenObjectType);
if (!objectName) {
// user cancelled
continue;
}
break;
}
} else {
// user selects table in tree view we use connection info from Object Explorer node
telemetryStep = CreateAzureFunctionStep.launchFromObjectExplorer;
connectionInfo = node.connectionInfo;
TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep)
.withAdditionalProperties(propertyBag).withConnectionInfo(connectionInfo).send();
// set the database containing the selected table or view 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;
}
}
// Prompt user for binding type
telemetryStep = CreateAzureFunctionStep.getBindingType;
let nodeType = ObjectType.Table === node.nodeType ? ObjectType.Table : ObjectType.View;
selectedBindingType = await azureFunctionsUtils.promptForBindingType(nodeType);
if (!selectedBindingType) {
// User cancelled
exitReason = ExitReason.cancelled;
return;
}
// send telemetry for object type and binding type
propertyBag.objectType = node.nodeType;
propertyBag.bindingType = selectedBindingType;
TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep)
.withAdditionalProperties(propertyBag).send();
objectName = utils.generateQuotedFullName(node.metadata.schema, node.metadata.name);
TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep)
.withAdditionalProperties(propertyBag).withConnectionInfo(connectionInfo).send();
}
// 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) {
// User cancelled
exitReason = ExitReason.cancelled;
return;
}
TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep)
.withAdditionalProperties(propertyBag)
.withConnectionInfo(connectionInfo).send();
// set the templateId based on the selected binding type
telemetryStep = CreateAzureFunctionStep.getTemplateId;
let templateId: string = selectedBindingType === BindingType.input ? constants.inputTemplateID : constants.outputTemplateID;
// prompt for Connection String Setting Name
let connectionStringInfo: IConnectionStringInfo | undefined = { connectionStringSettingName: constants.sqlConnectionStringSetting, connectionInfo: connectionInfo };
if (!isCreateNewProject && projectFile) {
// if it is not a new project, we can prompt user for connection string setting name and connection string password prompts
telemetryStep = CreateAzureFunctionStep.getConnectionStringSettingName;
connectionStringInfo = await azureFunctionsUtils.promptAndUpdateConnectionStringSetting(vscode.Uri.parse(projectFile), connectionInfo);
if (!connectionStringInfo) {
// User cancelled connection string setting name prompt or connection string method prompt
exitReason = ExitReason.cancelled;
return;
}
TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep)
.withAdditionalProperties(propertyBag)
.withConnectionInfo(connectionInfo).send();
}
// additional execution step that will be used by vscode-azurefunctions to execute only when creating a new azure function project
let connectionStringExecuteStep = createAddConnectionStringStep(projectFolder, connectionInfo, connectionStringInfo.connectionStringSettingName);
// create C# Azure Function with SQL Binding
telemetryStep = CreateAzureFunctionStep.createFunctionAPI;
await azureFunctionApi.createFunction({
language: 'C#',
targetFramework: 'netcoreapp3.1',
version: '~3',
templateId: templateId,
functionName: functionName,
functionSettings: {
connectionStringSetting: connectionStringInfo.connectionStringSettingName,
...(selectedBindingType === BindingType.input && { object: objectName }),
...(selectedBindingType === BindingType.output && { table: objectName })
},
folderPath: projectFolder,
suppressCreateProjectPrompt: true,
...(isCreateNewProject && { executeStep: connectionStringExecuteStep })
});
// Add latest sql extension package reference to project
await azureFunctionsUtils.addSqlNugetReferenceToProjectFile(projectFolder);
TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, telemetryStep)
.withAdditionalProperties(propertyBag)
.withConnectionInfo(connectionInfo).send();
telemetryStep = CreateAzureFunctionStep.finishCreateFunction;
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 ?? '';
// an error occurred during createFunction
exitReason = ExitReason.error;
void vscode.window.showErrorMessage(constants.errorNewAzureFunction(error));
TelemetryReporter.createErrorEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.exitCreateAzureFunctionQuickpick, undefined, errorType)
.withAdditionalProperties(propertyBag).send();
return;
} finally {
propertyBag.telemetryStep = telemetryStep ?? '';
propertyBag.exitReason = exitReason;
TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.exitCreateAzureFunctionQuickpick)
.withAdditionalProperties(propertyBag).send();
}
}
/**
* Adds a SQL Binding to a specified Azure function in a file
* @param bindingType Type of SQL Binding
* @param filePath Path of the file where the Azure Functions are
* @param functionName Name of the function where the SQL Binding is to be added
* @param objectName Name of Object for the SQL Query
* @param connectionStringSetting Setting for the connection string
* @returns Azure Function SQL binding
*/
export async function addSqlBinding(
bindingType: BindingType,
filePath: string,
functionName: string,
objectName: string,
connectionStringSetting: string
): Promise<ResultStatus> {
const params: AddSqlBindingParams = {
bindingType: bindingType,
filePath: filePath,
functionName: functionName,
objectName: objectName,
connectionStringSetting: connectionStringSetting
};
const vscodeMssqlApi = await utils.getVscodeMssqlApi();
return await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: constants.addSqlBinding(functionName),
cancellable: false
}, async (_progress, _token) => {
return vscodeMssqlApi.sendRequest(azureFunctionsContracts.AddSqlBindingRequest.type, params);
}
);
}
/**
* 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
*/
export async function getAzureFunctions(filePath: string): Promise<GetAzureFunctionsResult> {
const params: GetAzureFunctionsParams = {
filePath: filePath
};
const vscodeMssqlApi = await utils.getVscodeMssqlApi();
return vscodeMssqlApi.sendRequest(azureFunctionsContracts.GetAzureFunctionsRequest.type, params);
}