mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 02:51:36 -05:00
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
This commit is contained in:
@@ -40,8 +40,7 @@
|
|||||||
{
|
{
|
||||||
"command": "sqlBindings.createAzureFunction",
|
"command": "sqlBindings.createAzureFunction",
|
||||||
"title": "%sqlBindings.createAzureFunction%",
|
"title": "%sqlBindings.createAzureFunction%",
|
||||||
"category": "MS SQL",
|
"category": "MS SQL"
|
||||||
"when": "view == objectExplorer && viewItem == Table"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"menus": {
|
"menus": {
|
||||||
@@ -49,12 +48,17 @@
|
|||||||
{
|
{
|
||||||
"command": "sqlBindings.addSqlBinding",
|
"command": "sqlBindings.addSqlBinding",
|
||||||
"when": "editorLangId == csharp && !azdataAvailable && resourceScheme != untitled"
|
"when": "editorLangId == csharp && !azdataAvailable && resourceScheme != untitled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "sqlBindings.createAzureFunction",
|
||||||
|
"when": "view == objectExplorer && viewItem == Table && !azdataAvailable",
|
||||||
|
"group": "zAzure_Function@1"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"view/item/context": [
|
"view/item/context": [
|
||||||
{
|
{
|
||||||
"command": "sqlBindings.createAzureFunction",
|
"command": "sqlBindings.createAzureFunction",
|
||||||
"when": "view == objectExplorer && viewItem == Table",
|
"when": "view == objectExplorer && viewItem == Table && !azdataAvailable",
|
||||||
"group": "zAzure_Function@1"
|
"group": "zAzure_Function@1"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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 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 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 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
|
// Insert SQL binding
|
||||||
export const hostFileName = 'host.json';
|
export const hostFileName = 'host.json';
|
||||||
@@ -42,6 +44,7 @@ export const azureFunctionLocalSettingsFileName = 'local.settings.json';
|
|||||||
export const vscodeOpenCommand = 'vscode.open';
|
export const vscodeOpenCommand = 'vscode.open';
|
||||||
|
|
||||||
export const nameMustNotBeEmpty = localize('nameMustNotBeEmpty', "Name must not be empty");
|
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 yesString = localize('yesString', "Yes");
|
||||||
export const noString = localize('noString', "No");
|
export const noString = localize('noString', "No");
|
||||||
export const input = localize('input', "Input");
|
export const input = localize('input', "Input");
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import * as fs from 'fs';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as glob from 'fast-glob';
|
import * as glob from 'fast-glob';
|
||||||
import * as cp from 'child_process';
|
import * as cp from 'child_process';
|
||||||
|
import * as constants from '../common/constants';
|
||||||
|
|
||||||
export interface ValidationResult {
|
export interface ValidationResult {
|
||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
@@ -162,6 +163,30 @@ export function escapeClosingBrackets(str: string): string {
|
|||||||
return str.replace(']', ']]');
|
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
|
* Gets the package info for the extension based on where the extension is installed
|
||||||
* @returns the package info object
|
* @returns the package info object
|
||||||
|
|||||||
@@ -5,33 +5,18 @@
|
|||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { ITreeNodeInfo } from 'vscode-mssql';
|
import { ITreeNodeInfo } from 'vscode-mssql';
|
||||||
import { IExtension, BindingType } from 'sql-bindings';
|
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 { addSqlBinding, createAzureFunction, getAzureFunctions } from './services/azureFunctionsService';
|
||||||
import { launchAddSqlBindingQuickpick } from './dialogs/addSqlBindingQuickpick';
|
import { launchAddSqlBindingQuickpick } from './dialogs/addSqlBindingQuickpick';
|
||||||
import { promptForBindingType, promptAndUpdateConnectionStringSetting, promptForObjectName } from './common/azureFunctionsUtils';
|
import { promptForBindingType, promptAndUpdateConnectionStringSetting, promptForObjectName } from './common/azureFunctionsUtils';
|
||||||
|
|
||||||
export async function activate(context: vscode.ExtensionContext): Promise<IExtension> {
|
export async function activate(context: vscode.ExtensionContext): Promise<IExtension> {
|
||||||
const vscodeMssqlApi = await getVscodeMssqlApi();
|
|
||||||
void vscode.commands.executeCommand('setContext', 'azdataAvailable', !!getAzdataApi());
|
void vscode.commands.executeCommand('setContext', 'azdataAvailable', !!getAzdataApi());
|
||||||
// register the add sql binding command
|
// register the add sql binding command
|
||||||
context.subscriptions.push(vscode.commands.registerCommand('sqlBindings.addSqlBinding', async (uri: vscode.Uri | undefined) => { return launchAddSqlBindingQuickpick(uri); }));
|
context.subscriptions.push(vscode.commands.registerCommand('sqlBindings.addSqlBinding', async (uri: vscode.Uri | undefined) => { return launchAddSqlBindingQuickpick(uri); }));
|
||||||
// Generate Azure Function command
|
// Generate Azure Function command
|
||||||
context.subscriptions.push(vscode.commands.registerCommand('sqlBindings.createAzureFunction', async (node: ITreeNodeInfo) => {
|
context.subscriptions.push(vscode.commands.registerCommand('sqlBindings.createAzureFunction', async (node?: ITreeNodeInfo) => {
|
||||||
let connectionInfo = node.connectionInfo;
|
return await createAzureFunction(node);
|
||||||
// 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);
|
|
||||||
}));
|
}));
|
||||||
return {
|
return {
|
||||||
addSqlBinding: async (bindingType: BindingType, filePath: string, functionName: string, objectName: string, connectionStringSetting: string) => {
|
addSqlBinding: async (bindingType: BindingType, filePath: string, functionName: string, objectName: string, connectionStringSetting: string) => {
|
||||||
|
|||||||
@@ -12,15 +12,110 @@ import * as constants from '../common/constants';
|
|||||||
import * as azureFunctionsContracts from '../contracts/azureFunctions/azureFunctionsContracts';
|
import * as azureFunctionsContracts from '../contracts/azureFunctions/azureFunctionsContracts';
|
||||||
import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry';
|
import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry';
|
||||||
import { AddSqlBindingParams, BindingType, GetAzureFunctionsParams, GetAzureFunctionsResult, ResultStatus } from 'sql-bindings';
|
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 const hostFileName: string = 'host.json';
|
||||||
|
|
||||||
export async function createAzureFunction(connectionString: string, schema: string, table: string, connectionInfo: IConnectionInfo): Promise<void> {
|
export async function createAzureFunction(node?: ITreeNodeInfo): Promise<void> {
|
||||||
|
// telemetry properties for create azure function
|
||||||
let sessionId: string = uuid.v4();
|
let sessionId: string = uuid.v4();
|
||||||
let propertyBag: { [key: string]: string } = { sessionId: sessionId };
|
let propertyBag: { [key: string]: string } = { sessionId: sessionId };
|
||||||
let quickPickStep: string = '';
|
let quickPickStep: string = '';
|
||||||
let exitReason: string = 'cancelled';
|
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)
|
TelemetryReporter.createActionEvent(TelemetryViews.CreateAzureFunctionWithSqlBinding, TelemetryActions.startCreateAzureFunctionWithSqlBinding)
|
||||||
.withConnectionInfo(connectionInfo).send();
|
.withConnectionInfo(connectionInfo).send();
|
||||||
@@ -101,12 +196,14 @@ export async function createAzureFunction(connectionString: string, schema: stri
|
|||||||
try {
|
try {
|
||||||
// get function name from user
|
// get function name from user
|
||||||
quickPickStep = 'getAzureFunctionName';
|
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({
|
functionName = await vscode.window.showInputBox({
|
||||||
title: constants.functionNameTitle,
|
title: constants.functionNameTitle,
|
||||||
value: uniqueFunctionName,
|
value: uniqueFunctionName,
|
||||||
ignoreFocusOut: true,
|
ignoreFocusOut: true,
|
||||||
validateInput: input => input ? undefined : constants.nameMustNotBeEmpty
|
validateInput: input => utils.validateFunctionName(input)
|
||||||
}) as string;
|
}) as string;
|
||||||
if (!functionName) {
|
if (!functionName) {
|
||||||
return;
|
return;
|
||||||
@@ -115,26 +212,12 @@ export async function createAzureFunction(connectionString: string, schema: stri
|
|||||||
.withAdditionalProperties(propertyBag)
|
.withAdditionalProperties(propertyBag)
|
||||||
.withConnectionInfo(connectionInfo).send();
|
.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
|
// set the templateId based on the selected binding type
|
||||||
let templateId: string = selectedBinding.type === BindingType.input ? constants.inputTemplateID : constants.outputTemplateID;
|
let templateId: string = selectedBindingType === BindingType.input ? constants.inputTemplateID : constants.outputTemplateID;
|
||||||
let objectName = utils.generateQuotedFullName(schema, table);
|
|
||||||
|
|
||||||
// We need to set the azureWebJobsStorage to a placeholder
|
// We need to set the azureWebJobsStorage to a placeholder
|
||||||
// to suppress the warning for opening the wizard
|
// to suppress the warning for opening the wizard
|
||||||
// issue https://github.com/microsoft/azuredatastudio/issues/18780
|
// issue https://github.com/microsoft/azuredatastudio/issues/18780
|
||||||
|
|
||||||
await azureFunctionsUtils.setLocalAppSetting(path.dirname(projectFile), constants.azureWebJobsStorageSetting, constants.azureWebJobsStoragePlaceholder);
|
await azureFunctionsUtils.setLocalAppSetting(path.dirname(projectFile), constants.azureWebJobsStorageSetting, constants.azureWebJobsStoragePlaceholder);
|
||||||
|
|
||||||
// create C# Azure Function with SQL Binding
|
// create C# Azure Function with SQL Binding
|
||||||
@@ -144,8 +227,8 @@ export async function createAzureFunction(connectionString: string, schema: stri
|
|||||||
functionName: functionName,
|
functionName: functionName,
|
||||||
functionSettings: {
|
functionSettings: {
|
||||||
connectionStringSetting: constants.sqlConnectionStringSetting,
|
connectionStringSetting: constants.sqlConnectionStringSetting,
|
||||||
...(selectedBinding.type === BindingType.input && { object: objectName }),
|
...(selectedBindingType === BindingType.input && { object: objectName }),
|
||||||
...(selectedBinding.type === BindingType.output && { table: objectName })
|
...(selectedBindingType === BindingType.output && { table: objectName })
|
||||||
},
|
},
|
||||||
folderPath: projectFile
|
folderPath: projectFile
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user