mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-13 17:22:15 -05:00
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:
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = '******';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
125
extensions/sql-bindings/src/services/azureFunctionsService.ts
Normal file
125
extensions/sql-bindings/src/services/azureFunctionsService.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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: '',
|
||||
|
||||
86
extensions/sql-bindings/src/typings/vscode-azurefunctions.api.d.ts
vendored
Normal file
86
extensions/sql-bindings/src/typings/vscode-azurefunctions.api.d.ts
vendored
Normal 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[];
|
||||
}
|
||||
23
extensions/sql-bindings/src/typings/vscode-azuretools.api.d.ts
vendored
Normal file
23
extensions/sql-bindings/src/typings/vscode-azuretools.api.d.ts
vendored
Normal 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;
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user