Refactor vscode-mssql sql bindings logic to sql bindings ext (#18725)

* wip for refactor of mssql to sql-bindings

* remove STS dependency

* work to bring function over and setup with vscodeMsql APIs

* copy typings from vscode-mssql
This commit is contained in:
Vasu Bhog
2022-03-14 13:07:27 -07:00
committed by GitHub
parent a86301312c
commit 2d1ffeb47c
12 changed files with 627 additions and 81 deletions

View File

@@ -13,7 +13,8 @@
"icon": "",
"aiKey": "AIF-37eefaf0-8022-4671-a3fb-64752724682e",
"activationEvents": [
"onCommand:sqlBindings.addSqlBinding"
"onCommand:sqlBindings.addSqlBinding",
"onCommand:sqlBindings.createAzureFunction"
],
"main": "./out/extension",
"repository": {
@@ -35,6 +36,12 @@
"command": "sqlBindings.addSqlBinding",
"title": "%sqlBindings.addSqlBinding%",
"category": "MS SQL"
},
{
"command": "sqlBindings.createAzureFunction",
"title": "%sqlBindings.createAzureFunction%",
"category": "MS SQL",
"when": "view == objectExplorer && viewItem == Table"
}
],
"menus": {
@@ -43,19 +50,25 @@
"command": "sqlBindings.addSqlBinding",
"when": "editorLangId == csharp && !azdataAvailable && resourceScheme != untitled"
}
],
"view/item/context": [
{
"command": "sqlBindings.createAzureFunction",
"when": "view == objectExplorer && viewItem == Table",
"group": "zAzure_Function@1"
}
]
}
},
"dependencies": {
"@microsoft/ads-extension-telemetry": "^1.1.5",
"fast-glob": "^3.2.7",
"fs-extra": "^5.0.0",
"jsonc-parser": "^2.3.1",
"promisify-child-process": "^3.1.1",
"vscode-nls": "^4.1.2"
},
"devDependencies": {
"@types/fs-extra": "^5.0.0",
"@types/node": "^14.14.16",
"tslint": "^5.8.0",
"should": "^13.2.1",
"sinon": "^9.0.2",

View File

@@ -1,5 +1,6 @@
{
"displayName": "SQL Bindings",
"description": "Enables users to develop and publish Azure Functions with Azure SQL bindings",
"sqlBindings.addSqlBinding": "Add SQL Binding (preview)"
"sqlBindings.addSqlBinding": "Add SQL Binding (preview)",
"sqlBindings.createAzureFunction": "Create Azure Function with SQL binding"
}

View File

@@ -2,13 +2,16 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fse from 'fs-extra';
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 { parseJson } from './parseJson';
// 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
import { AzureExtensionApiProvider } from '../typings/vscode-azuretools.api';
/**
* Represents the settings in an Azure function project's locawl.settings.json file
*/
@@ -19,20 +22,25 @@ export interface ILocalSettingsJson {
ConnectionStrings?: { [key: string]: string };
}
export interface IFileFunctionObject {
filePromise: Promise<string>;
watcherDisposable: vscode.Disposable;
}
/**
* copied and modified from vscode-azurefunctions extension
* https://github.com/microsoft/vscode-azurefunctions/blob/main/src/funcConfig/local.settings.ts
* @param localSettingsPath full path to local.settings.json
* @returns settings in local.settings.json. If no settings are found, returns default "empty" settings
*/
export async function getLocalSettingsJson(localSettingsPath: string): Promise<ILocalSettingsJson> {
if (await fse.pathExists(localSettingsPath)) {
const data: string = (await fse.readFile(localSettingsPath)).toString();
if (/[^\s]/.test(data)) {
try {
return parseJson(data);
} catch (error) {
throw new Error(constants.failedToParse(error.message));
}
if (fs.existsSync(localSettingsPath)) {
const data: string = (fs.readFileSync(localSettingsPath)).toString();
try {
return JSON.parse(data);
} catch (error) {
console.log(error);
throw new Error(utils.formatString(constants.failedToParse(error.message), constants.azureFunctionLocalSettingsFileName, error.message));
}
}
@@ -66,11 +74,210 @@ export async function setLocalAppSetting(projectFolder: string, key: string, val
}
settings.Values[key] = value;
await fse.writeJson(localSettingsPath, settings, { spaces: 2 });
void fs.promises.writeFile(localSettingsPath, JSON.stringify(settings, undefined, 2));
return true;
}
/**
* Gets the Azure Functions extension API if it is installed
* if it is not installed, prompt the user to install directly, learn more, or do not install
* @returns the Azure Functions extension API if it is installed, prompt if it is not installed
*/
export async function getAzureFunctionsExtensionApi(): Promise<AzureFunctionsExtensionApi | undefined> {
let apiProvider = await vscode.extensions.getExtension(constants.azureFunctionsExtensionName)?.activate() as AzureExtensionApiProvider;
if (!apiProvider) {
const response = await vscode.window.showInformationMessage(constants.azureFunctionsExtensionNotFound,
constants.install, constants.learnMore, constants.doNotInstall);
if (response === constants.install) {
const extensionInstalled = new Promise<void>((resolve, reject) => {
const timeout = setTimeout(async () => {
reject(new Error(constants.timeoutExtensionError));
extensionChange.dispose();
}, 10000);
let extensionChange = vscode.extensions.onDidChange(async () => {
if (vscode.extensions.getExtension(constants.azureFunctionsExtensionName)) {
resolve();
extensionChange.dispose();
clearTimeout(timeout);
}
});
});
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: constants.azureFunctionsExtensionName,
cancellable: false
}, async (_progress, _token) => {
await vscode.commands.executeCommand('workbench.extensions.installExtension', constants.azureFunctionsExtensionName);
}
);
// the extension has not been notified that the azure function extension is installed so wait till it is to then activate it
await extensionInstalled;
apiProvider = await vscode.extensions.getExtension(constants.azureFunctionsExtensionName)?.activate() as AzureExtensionApiProvider;
} else if (response === constants.learnMore) {
await vscode.env.openExternal(vscode.Uri.parse(constants.linkToAzureFunctionExtension));
return undefined;
} else {
return undefined;
}
}
const azureFunctionApi = apiProvider.getApi<AzureFunctionsExtensionApi>('*');
if (azureFunctionApi) {
return azureFunctionApi;
} else {
void vscode.window.showErrorMessage(constants.azureFunctionsExtensionNotInstalled);
return undefined;
}
}
/**
* TODO REMOVE defaultSqlBindingTextLines
* Overwrites the Azure function methods body to work with the binding
* @param filePath is the path for the function file (.cs for C# functions)
*/
export function overwriteAzureFunctionMethodBody(filePath: string): void {
let defaultBindedFunctionText = fs.readFileSync(filePath, 'utf-8');
// Replace default binding text
let newValueLines = defaultBindedFunctionText.split(os.EOL);
const defaultFunctionTextToSkip = new Set(constants.defaultSqlBindingTextLines);
let replacedValueLines = [];
for (let defaultLine of newValueLines) {
// Skipped lines
if (defaultFunctionTextToSkip.has(defaultLine.trimStart())) {
continue;
} else if (defaultLine.trimStart() === constants.defaultBindingResult) { // Result change
replacedValueLines.push(defaultLine.replace(constants.defaultBindingResult, constants.sqlBindingResult));
} else {
// Normal lines to be included
replacedValueLines.push(defaultLine);
}
}
defaultBindedFunctionText = replacedValueLines.join(os.EOL);
fs.writeFileSync(filePath, defaultBindedFunctionText, 'utf-8');
}
/**
* Gets the azure function project for the user to choose from a list of projects files
* If only one project is found that project is used to add the binding to
* if no project is found, user is informed there needs to be a C# Azure Functions project
* @returns the selected project file path
*/
export async function getAzureFunctionProject(): Promise<string | undefined> {
let selectedProjectFile: string | undefined = '';
if (vscode.workspace.workspaceFolders === undefined || vscode.workspace.workspaceFolders.length === 0) {
return selectedProjectFile;
} else {
const projectFiles = await getAzureFunctionProjectFiles();
if (projectFiles !== undefined) {
if (projectFiles.length > 1) {
// select project to add azure function to
selectedProjectFile = (await vscode.window.showQuickPick(projectFiles, {
canPickMany: false,
title: constants.selectProject,
ignoreFocusOut: true
}));
return selectedProjectFile;
} else if (projectFiles.length === 1) {
// only one azure function project found
return projectFiles[0];
}
}
return undefined;
}
}
/**
* Gets the azure function project files based on the host file found in the same folder
* @returns the azure function project files paths
*/
export async function getAzureFunctionProjectFiles(): Promise<string[] | undefined> {
let projFiles: string[] = [];
const hostFiles = await getHostFiles();
if (!hostFiles) {
return undefined;
}
for (let host of hostFiles) {
let projectFile = await vscode.workspace.findFiles(new vscode.RelativePattern(path.dirname(host), '*.csproj'));
projectFile.filter(file => path.dirname(file.fsPath) === path.dirname(host) ? projFiles.push(file?.fsPath) : projFiles);
}
return projFiles.length > 0 ? projFiles : undefined;
}
/**
* Gets the host files from the workspace
* @returns the host file paths
*/
export async function getHostFiles(): Promise<string[] | undefined> {
const hostUris = await vscode.workspace.findFiles('**/host.json');
const hostFiles = hostUris.map(uri => uri.fsPath);
return hostFiles.length > 0 ? hostFiles : undefined;
}
/**
* Gets the local.settings.json file path
* @param projectFile path of the azure function project
* @returns the local.settings.json file path
*/
export async function getSettingsFile(projectFile: string): Promise<string | undefined> {
return path.join(path.dirname(projectFile), '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
* @returns the function file path once created and the watcher disposable
*/
export function waitForNewFunctionFile(projectFile: string): IFileFunctionObject {
const watcher = vscode.workspace.createFileSystemWatcher((
path.dirname(projectFile), '**/*.cs'), false, true, true);
const filePromise = new Promise<string>((resolve, _) => {
watcher.onDidCreate((e) => {
resolve(e.fsPath);
});
});
return {
filePromise,
watcherDisposable: watcher
};
}
/**
* Retrieves the new host project file once it has created and the watcher disposable
* @returns the host file path once created and the watcher disposable
*/
export function waitForNewHostFile(): IFileFunctionObject {
const watcher = vscode.workspace.createFileSystemWatcher('**/host.json', false, true, true);
const filePromise = new Promise<string>((resolve, _) => {
watcher.onDidCreate((e) => {
resolve(e.fsPath);
});
});
return {
filePromise,
watcherDisposable: watcher
};
}
/**
* Adds the required nuget package to the project
* @param selectedProjectFile is the users selected project file path
*/
export async function addNugetReferenceToProjectFile(selectedProjectFile: string): Promise<void> {
await utils.executeCommand(`dotnet add ${selectedProjectFile} package ${constants.sqlExtensionPackageName} --prerelease`);
}
/**
* Adds the Sql Connection String to the local.settings.json
* @param connectionString of the SQL Server connection that was chosen by the user
*/
export async function addConnectionStringToConfig(connectionString: string, projectFile: string): Promise<void> {
const settingsFile = await getSettingsFile(projectFile);
if (settingsFile) {
await setLocalAppSetting(path.dirname(settingsFile), constants.sqlConnectionString, connectionString);
}
}
/**
* Gets the Azure Functions project that contains the given file if the project is open in one of the workspace folders
* @param fileUri file that the containing project needs to be found for
@@ -98,13 +305,5 @@ export async function getAFProjectContainingFile(fileUri: vscode.Uri): Promise<v
// Use 'host.json' as an indicator that this is a functions project
// copied from verifyIsproject.ts in vscode-azurefunctions extension
export async function isFunctionProject(folderPath: string): Promise<boolean> {
return fse.pathExists(path.join(folderPath, constants.hostFileName));
return fs.existsSync(path.join(folderPath, constants.hostFileName));
}
/**
* Adds the required nuget package to the project
* @param selectedProjectFile is the users selected project file path
*/
export async function addNugetReferenceToProjectFile(selectedProjectFile: string): Promise<void> {
await utils.executeCommand(`dotnet add ${selectedProjectFile} package ${constants.sqlExtensionPackageName} --prerelease`);
}

View File

@@ -8,9 +8,40 @@ import * as utils from '../common/utils';
const localize = nls.loadMessageBundle();
// Azure Functions
export const azureFunctionsExtensionName = 'ms-azuretools.vscode-azurefunctions';
export const sqlConnectionString = 'SqlConnectionString';
export const linkToAzureFunctionExtension = 'https://docs.microsoft.com/azure/azure-functions/functions-develop-vs-code';
export const sqlBindingsDoc = 'https://aka.ms/sqlbindings';
export const defaultSqlBindingTextLines =
[
'log.LogInformation(\"C# HTTP trigger function processed a request.\");',
'string name = req.Query[\"name\"];',
'string requestBody = await new StreamReader(req.Body).ReadToEndAsync();',
'dynamic data = JsonConvert.DeserializeObject(requestBody);',
'name = name ?? data?.name;',
'string responseMessage = string.IsNullOrEmpty(name) ? \"This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.\" : $\"Hello, {name}. This HTTP triggered function executed successfully.\";'
];
export const defaultBindingResult = 'return new OkObjectResult(responseMessage);';
export const sqlBindingResult = `return new OkObjectResult(result);`;
export const sqlExtensionPackageName = 'Microsoft.Azure.WebJobs.Extensions.Sql';
export const functionNameTitle = localize('functionNameTitle', 'Function Name');
export const selectProject = localize('selectProject', 'Select the Azure Function project for the SQL Binding');
export const azureFunctionsExtensionNotFound = localize('azureFunctionsExtensionNotFound', 'The Azure Functions extension is required to create a new Azure Function with SQL binding but is not installed, install it now?');
export const install = localize('install', 'Install');
export const learnMore = localize('learnMore', 'Learn more');
export const doNotInstall = localize('doNotInstall', 'Do not install');
export const createProject = localize('createProject', 'Create Azure Function Project');
export const selectAzureFunctionProjFolder = localize('selectAzureFunctionProjFolder', 'Select folder for the Azure Function project');
export const timeoutExtensionError = localize('timeoutExtensionError', 'Timed out waiting for extension to install');
export const timeoutAzureFunctionFileError = localize('timeoutAzureFunctionFileError', 'Timed out waiting for Azure Function file to be created');
export const timeoutProjectError = localize('timeoutProjectError', 'Timed out waiting for project to be created');
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.');
// Insert SQL binding
export const hostFileName = 'host.json';
export const sqlExtensionPackageName = 'Microsoft.Azure.WebJobs.Extensions.Sql';
export const placeHolderObject = '[dbo].[table1]';
export const sqlBindingsHelpLink = 'https://github.com/Azure/azure-functions-sql-extension/blob/main/README.md';
export const passwordPlaceholder = '******';

View File

@@ -6,7 +6,7 @@
import type * as azdataType from 'azdata';
import * as vscode from 'vscode';
import * as vscodeMssql from 'vscode-mssql';
import * as fse from 'fs-extra';
import * as fs from 'fs';
import * as path from 'path';
import * as glob from 'fast-glob';
import * as cp from 'child_process';
@@ -16,6 +16,13 @@ export interface ValidationResult {
validated: boolean
}
export interface IPackageInfo {
name: string;
fullName: string;
version: string;
aiKey: string;
}
/**
* Consolidates on the error message string
*/
@@ -40,13 +47,6 @@ export async function getVscodeMssqlApi(): Promise<vscodeMssql.IExtension> {
return ext.activate();
}
export interface IPackageInfo {
name: string;
fullName: string;
version: string;
aiKey: string;
}
// Try to load the azdata API - but gracefully handle the failure in case we're running
// in a context where the API doesn't exist (such as VS Code)
let azdataApi: typeof azdataType | undefined = undefined;
@@ -68,14 +68,6 @@ export function getAzdataApi(): typeof azdataType | undefined {
return azdataApi;
}
export async function createFolderIfNotExist(folderPath: string): Promise<void> {
try {
await fse.mkdir(folderPath);
} catch {
// Ignore if failed
}
}
export async function executeCommand(command: string, cwd?: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
cp.exec(command, { maxBuffer: 500 * 1024, cwd: cwd }, (error: Error | null, stdout: string, stderr: string) => {
@@ -109,6 +101,75 @@ export async function getAllProjectsInFolder(folder: vscode.Uri, projectExtensio
return (await glob(projFilter)).map(p => vscode.Uri.file(path.resolve(p)));
}
/**
* Format a string. Behaves like C#'s string.Format() function.
*/
export function formatString(str: string, ...args: any[]): string {
// This is based on code originally from https://github.com/Microsoft/vscode/blob/master/src/vs/nls.js
// License: https://github.com/Microsoft/vscode/blob/master/LICENSE.txt
let result: string;
if (args.length === 0) {
result = str;
} else {
result = str.replace(/\{(\d+)\}/g, (match, rest) => {
let index = rest[0];
return typeof args[index] !== 'undefined' ? args[index] : match;
});
}
return result;
}
/**
* Generates a quoted full name for the object
* @param schema of the object
* @param objectName object chosen by the user
* @returns the quoted and escaped full name of the specified schema and object
*/
export function generateQuotedFullName(schema: string, objectName: string): string {
return `[${escapeClosingBrackets(schema)}].[${escapeClosingBrackets(objectName)}]`;
}
/**
* Returns a promise that will reject after the specified timeout
* @param errorMessage error message to be returned in the rejection
* @param ms timeout in milliseconds. Default is 10 seconds
* @returns a promise that rejects after the specified timeout
*/
export function timeoutPromise(errorMessage: string, ms: number = 10000): Promise<string> {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(errorMessage));
}, ms);
});
}
/**
* 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
* @returns a promise with the unique file name, or undefined
*/
export async function getUniqueFileName(folderPath: string, fileName: string): Promise<string | undefined> {
let count: number = 0;
const maxCount: number = 1024;
let uniqueFileName = fileName;
while (count < maxCount) {
if (!fs.existsSync(path.join(folderPath, uniqueFileName + '.cs'))) {
return uniqueFileName;
}
count += 1;
uniqueFileName = fileName + count.toString();
}
return undefined;
}
export function escapeClosingBrackets(str: string): string {
return str.replace(']', ']]');
}
/**
* Gets the package info for the extension based on where the extension is installed
* @returns the package info object

View File

@@ -3,12 +3,35 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { getAzdataApi } from './common/utils';
import { ITreeNodeInfo } from 'vscode-mssql';
import { getAzdataApi, getVscodeMssqlApi } from './common/utils';
import { launchAddSqlBindingQuickpick } from './dialogs/addSqlBindingQuickpick';
import { createAzureFunction } from './services/azureFunctionsService';
export async function activate(context: vscode.ExtensionContext): Promise<void> {
const vscodeMssqlApi = await getVscodeMssqlApi();
export function activate(context: vscode.ExtensionContext): void {
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);
}));
}
export function deactivate(): void {

View File

@@ -0,0 +1,125 @@
/*---------------------------------------------------------------------------------------------
* 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 mssql from 'vscode-mssql';
import * as path from 'path';
import * as utils from '../common/utils';
import * as azureFunctionUtils from '../common/azureFunctionsUtils';
import * as constants from '../common/constants';
export const hostFileName: string = 'host.json';
export async function createAzureFunction(connectionString: string, schema: string, table: string): Promise<void> {
const azureFunctionApi = await azureFunctionUtils.getAzureFunctionsExtensionApi();
if (!azureFunctionApi) {
return;
}
let projectFile = await azureFunctionUtils.getAzureFunctionProject();
let newHostProjectFile!: azureFunctionUtils.IFileFunctionObject;
let hostFile: string;
if (!projectFile) {
let projectCreate = await vscode.window.showErrorMessage(constants.azureFunctionsProjectMustBeOpened,
constants.createProject, constants.learnMore);
if (projectCreate === constants.learnMore) {
void vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(constants.sqlBindingsDoc));
return;
} else if (projectCreate === constants.createProject) {
// start the create azure function project flow
try {
// 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 = await azureFunctionUtils.waitForNewHostFile();
await azureFunctionApi.createFunction({});
const timeoutForHostFile = utils.timeoutPromise(constants.timeoutProjectError);
hostFile = await Promise.race([newHostProjectFile.filePromise, timeoutForHostFile]);
if (hostFile) {
// start the add sql binding flow
projectFile = await azureFunctionUtils.getAzureFunctionProject();
}
} catch (error) {
void vscode.window.showErrorMessage(utils.formatString(constants.errorNewAzureFunction, error.message ?? error));
return;
} finally {
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 = azureFunctionUtils.waitForNewFunctionFile(projectFile);
let functionFile: string;
let functionName: string;
try {
// get function name from user
let uniqueFunctionName = await utils.getUniqueFileName(path.dirname(projectFile), table);
functionName = await vscode.window.showInputBox({
title: constants.functionNameTitle,
value: uniqueFunctionName,
ignoreFocusOut: true,
validateInput: input => input ? undefined : constants.nameMustNotBeEmpty
}) as string;
if (!functionName) {
return;
}
// create C# HttpTrigger
await azureFunctionApi.createFunction({
language: 'C#',
templateId: 'HttpTrigger',
functionName: functionName,
folderPath: projectFile
});
// check for the new function file to be created and dispose of the file system watcher
const timeoutForFunctionFile = utils.timeoutPromise(constants.timeoutAzureFunctionFileError);
functionFile = await Promise.race([newFunctionFileObject.filePromise, timeoutForFunctionFile]);
} finally {
newFunctionFileObject.watcherDisposable.dispose();
}
// select input or output binding
const inputOutputItems: (vscode.QuickPickItem & { type: mssql.BindingType })[] = [
{
label: constants.input,
type: mssql.BindingType.input
},
{
label: constants.output,
type: mssql.BindingType.output
}
];
const selectedBinding = await vscode.window.showQuickPick(inputOutputItems, {
canPickMany: false,
title: constants.selectBindingType,
ignoreFocusOut: true
});
if (!selectedBinding) {
return;
}
await azureFunctionUtils.addNugetReferenceToProjectFile(projectFile);
await azureFunctionUtils.addConnectionStringToConfig(connectionString, projectFile);
let objectName = utils.generateQuotedFullName(schema, table);
const azureFunctionsService = await utils.getAzureFunctionService();
await azureFunctionsService.addSqlBinding(
selectedBinding.type,
functionFile,
functionName,
objectName,
constants.sqlConnectionString
);
azureFunctionUtils.overwriteAzureFunctionMethodBody(functionFile);
}
}

View File

@@ -269,6 +269,9 @@ export class MockVscodeMssqlIExtension implements vscodeMssql.IExtension {
getConnectionString(_: string | vscodeMssql.ConnectionDetails, ___?: boolean, _____?: boolean): Promise<string> {
throw new Error('Method not implemented.');
}
createConnectionDetails(_: vscodeMssql.IConnectionInfo): vscodeMssql.ConnectionDetails {
throw new Error('Method not implemented.');
}
}
export function createTestUtils(): TestUtils {
@@ -351,6 +354,7 @@ export function createTestCredentials(): vscodeMssql.IConnectionInfo {
password: '12345678',
email: 'test-email',
accountId: 'test-account-id',
tenantId: 'test-tenant-id',
port: 1234,
authenticationType: 'test',
azureAccountToken: '',

View File

@@ -0,0 +1,86 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface AzureFunctionsExtensionApi {
apiVersion: string;
revealTreeItem(resourceId: string): Promise<void>;
createFunction(options: ICreateFunctionOptions): Promise<void>;
downloadAppSettings(client: IAppSettingsClient): Promise<void>;
uploadAppSettings(client: IAppSettingsClient, exclude?: (RegExp | string)[]): Promise<void>;
}
export type ProjectLanguage = 'JavaScript' | 'TypeScript' | 'C#' | 'Python' | 'PowerShell' | 'Java';
export type ProjectVersion = '~1' | '~2' | '~3' | '~4';
export interface IAppSettingsClient {
fullName: string;
listApplicationSettings(): Promise<IStringDictionary>;
updateApplicationSettings(appSettings: IStringDictionary): Promise<IStringDictionary>;
}
interface IStringDictionary {
properties?: { [propertyName: string]: string };
}
/**
* The options to use when creating a function. If an option is not specified, the default will be used or the user will be prompted
*/
export interface ICreateFunctionOptions {
/**
* The folder containing the Azure Functions project
*/
folderPath?: string;
/**
* The name of the function
*/
functionName?: string;
/**
* The language of the project
*/
language?: ProjectLanguage;
/**
* A filter specifying the langauges to display when creating a project (if there's not already a project)
*/
languageFilter?: RegExp;
/**
* The version of the project. Defaults to the latest GA version
*/
version?: ProjectVersion;
/**
* The id of the template to use.
* NOTE: The language part of the id is optional. Aka "HttpTrigger" will work just as well as "HttpTrigger-JavaScript"
*/
templateId?: string;
/**
* A case-insensitive object of settings to use for the function
*/
functionSettings?: {
[key: string]: string | undefined
}
/**
* If set to true, it will automatically create a new project without prompting (if there's not already a project). Defaults to false
*/
suppressCreateProjectPrompt?: boolean;
/**
* If set to true, it will not try to open the folder after create finishes. Defaults to false
*/
suppressOpenFolder?: boolean;
/**
* If set, it will automatically select the worker runtime for .NET with the matching targetFramework
*/
targetFramework?: string | string[];
}

View File

@@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// For now this file needs to be copied/pasted into your repo if you want the types. Eventually we may put it somewhere more distributable.
export interface AzureExtensionApi {
/**
* The API version for this extension. It should be versioned separately from the extension and ideally remains backwards compatible.
*/
apiVersion: string;
}
export interface AzureExtensionApiProvider {
/**
* Provides the API for an Azure Extension.
*
* @param apiVersionRange The version range of the API you need. Any semver syntax is allowed. For example "1" will return any "1.x.x" version or "1.2" will return any "1.2.x" version
* @throws Error if a matching version is not found.
*/
getApi<T extends AzureExtensionApi>(apiVersionRange: string): T;
}

View File

@@ -84,6 +84,14 @@ declare module 'vscode-mssql' {
* @returns connection string for the connection
*/
getConnectionString(connectionUriOrDetails: string | ConnectionDetails, includePassword?: boolean, includeApplicationName?: boolean): Promise<string>;
/**
* Set connection details for the provided connection info
* Able to use this for getConnectionString requests to STS that require ConnectionDetails type
* @param connectionInfo connection info of the connection
* @returns connection details credentials for the connection
*/
createConnectionDetails(connectionInfo: IConnectionInfo): ConnectionDetails;
}
/**
@@ -120,6 +128,11 @@ declare module 'vscode-mssql' {
*/
accountId: string | undefined;
/**
* tenantId
*/
tenantId: string | undefined;
/**
* The port number to connect to.
*/

View File

@@ -293,17 +293,10 @@
resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5"
integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==
"@types/fs-extra@^5.0.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-5.1.0.tgz#2a325ef97901504a3828718c390d34b8426a10a1"
integrity sha512-AInn5+UBFIK9FK5xc9yP5e3TQSPNNgjHByqYcj9g5elVBnDQcQL7PlO1CIRy2gWlbwK7UPYqi7vRvFA44dCmYQ==
dependencies:
"@types/node" "*"
"@types/node@*":
version "17.0.21"
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.21.tgz#864b987c0c68d07b4345845c3e63b75edd143644"
integrity sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ==
"@types/node@^14.14.16":
version "14.18.12"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.12.tgz#0d4557fd3b94497d793efd4e7d92df2f83b4ef24"
integrity sha512-q4jlIR71hUpWTnGhXWcakgkZeHa3CCjcQcnuzU8M891BAWA2jHiziiWEPEkdS5pFsz7H9HJiy8BrK7tBRNrY7A==
ansi-regex@^3.0.0:
version "3.0.0"
@@ -591,15 +584,6 @@ fill-range@^7.0.1:
dependencies:
to-regex-range "^5.0.1"
fs-extra@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-5.0.0.tgz#414d0110cdd06705734d055652c5411260c31abd"
integrity sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==
dependencies:
graceful-fs "^4.1.2"
jsonfile "^4.0.0"
universalify "^0.1.0"
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@@ -651,11 +635,6 @@ globals@^11.1.0:
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
graceful-fs@^4.1.2, graceful-fs@^4.1.6:
version "4.2.9"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96"
integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==
growl@1.10.5:
version "1.10.5"
resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"
@@ -820,13 +799,6 @@ jsonc-parser@^2.3.1:
resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.3.1.tgz#59549150b133f2efacca48fe9ce1ec0659af2342"
integrity sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==
jsonfile@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
optionalDependencies:
graceful-fs "^4.1.6"
just-extend@^4.0.2:
version "4.2.1"
resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744"
@@ -1259,11 +1231,6 @@ typemoq@^2.1.0:
lodash "^4.17.4"
postinstall-build "^5.0.1"
universalify@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
vscode-extension-telemetry@^0.1.6:
version "0.1.7"
resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.7.tgz#18389bc24127c89dade29cd2b71ba69a6ee6ad26"