mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-21 01:25:37 -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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user