mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-27 01:25:36 -05:00
Refactor SQL Binding code over from sql-database-projects (#18674)
* add sql binding prompt / logic * remove controller and move to extension * remove unused dependencies
This commit is contained in:
110
extensions/sql-bindings/src/common/azureFunctionsUtils.ts
Normal file
110
extensions/sql-bindings/src/common/azureFunctionsUtils.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import * as utils from './utils';
|
||||
import * as constants from './constants';
|
||||
import { parseJson } from './parseJson';
|
||||
|
||||
/**
|
||||
* Represents the settings in an Azure function project's locawl.settings.json file
|
||||
*/
|
||||
export interface ILocalSettingsJson {
|
||||
IsEncrypted?: boolean;
|
||||
Values?: { [key: string]: string };
|
||||
Host?: { [key: string]: string };
|
||||
ConnectionStrings?: { [key: string]: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* copied and modified from vscode-azurefunctions extension
|
||||
* @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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
IsEncrypted: false // Include this by default otherwise the func cli assumes settings are encrypted and fails to run
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new setting to a project's local.settings.json file
|
||||
* modified from setLocalAppSetting code from vscode-azurefunctions extension
|
||||
* @param projectFolder full path to project folder
|
||||
* @param key Key of the new setting
|
||||
* @param value Value of the new setting
|
||||
* @returns true if successful adding the new setting, false if unsuccessful
|
||||
*/
|
||||
export async function setLocalAppSetting(projectFolder: string, key: string, value: string): Promise<boolean> {
|
||||
const localSettingsPath: string = path.join(projectFolder, constants.azureFunctionLocalSettingsFileName);
|
||||
const settings: ILocalSettingsJson = await getLocalSettingsJson(localSettingsPath);
|
||||
|
||||
settings.Values = settings.Values || {};
|
||||
if (settings.Values[key] === value) {
|
||||
// don't do anything if it's the same as the existing value
|
||||
return true;
|
||||
} else if (settings.Values[key]) {
|
||||
const result = await vscode.window.showWarningMessage(constants.settingAlreadyExists(key), { modal: true }, constants.yesString);
|
||||
if (result !== constants.yesString) {
|
||||
// key already exists and user doesn't want to overwrite it
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
settings.Values[key] = value;
|
||||
await fse.writeJson(localSettingsPath, settings, { spaces: 2 });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @returns uri of project or undefined if project couldn't be found
|
||||
*/
|
||||
export async function getAFProjectContainingFile(fileUri: vscode.Uri): Promise<vscode.Uri | undefined> {
|
||||
// get functions csprojs in the workspace
|
||||
const projectPromises = vscode.workspace.workspaceFolders?.map(f => utils.getAllProjectsInFolder(f.uri, '.csproj')) ?? [];
|
||||
const functionsProjects = (await Promise.all(projectPromises)).reduce((prev, curr) => prev.concat(curr), []).filter(p => isFunctionProject(path.dirname(p.fsPath)));
|
||||
|
||||
// look for project folder containing file if there's more than one
|
||||
if (functionsProjects.length > 1) {
|
||||
// TODO: figure out which project contains the file
|
||||
// the new style csproj doesn't list all the files in the project anymore, unless the file isn't in the same folder
|
||||
// so we can't rely on using that to check
|
||||
console.error('need to find which project contains the file ' + fileUri.fsPath);
|
||||
return undefined;
|
||||
} else if (functionsProjects.length === 0) {
|
||||
throw new Error(constants.noAzureFunctionsProjectsInWorkspace);
|
||||
} else {
|
||||
return functionsProjects[0];
|
||||
}
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
/**
|
||||
* 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`);
|
||||
}
|
||||
|
||||
59
extensions/sql-bindings/src/common/constants.ts
Normal file
59
extensions/sql-bindings/src/common/constants.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
import * as utils from '../common/utils';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
// 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 = '******';
|
||||
export const azureFunctionLocalSettingsFileName = 'local.settings.json';
|
||||
export const vscodeOpenCommand = 'vscode.open';
|
||||
|
||||
export const nameMustNotBeEmpty = localize('nameMustNotBeEmpty', "Name must not be empty");
|
||||
export const yesString = localize('yesString', "Yes");
|
||||
export const noString = localize('noString', "No");
|
||||
export const input = localize('input', "Input");
|
||||
export const output = localize('output', "Output");
|
||||
export const selectBindingType = localize('selectBindingType', "Select type of binding");
|
||||
export const selectAzureFunction = localize('selectAzureFunction', "Select an Azure function in the current file to add SQL binding to");
|
||||
export const sqlTableOrViewToQuery = localize('sqlTableOrViewToQuery', "SQL table or view to query");
|
||||
export const sqlTableToUpsert = localize('sqlTableToUpsert', "SQL table to upsert into");
|
||||
export const connectionStringSetting = localize('connectionStringSetting', "Connection string setting name");
|
||||
export const selectSetting = localize('selectSetting', "Select SQL connection string setting from local.settings.json");
|
||||
export const connectionStringSettingPlaceholder = localize('connectionStringSettingPlaceholder', "Connection string setting specified in \"local.settings.json\"");
|
||||
export const noAzureFunctionsInFile = localize('noAzureFunctionsInFile', "No Azure functions in the current active file");
|
||||
export const noAzureFunctionsProjectsInWorkspace = localize('noAzureFunctionsProjectsInWorkspace', "No Azure functions projects found in the workspace");
|
||||
export const addPackage = localize('addPackage', "Add Package");
|
||||
export const createNewLocalAppSetting = localize('createNewLocalAppSetting', 'Create new local app setting');
|
||||
export const createNewLocalAppSettingWithIcon = `$(add) ${createNewLocalAppSetting}`;
|
||||
export const sqlConnectionStringSetting = 'SqlConnectionString';
|
||||
export const valueMustNotBeEmpty = localize('valueMustNotBeEmpty', "Value must not be empty");
|
||||
export const enterConnectionStringSettingName = localize('enterConnectionStringSettingName', "Enter connection string setting name");
|
||||
export const enterConnectionString = localize('enterConnectionString', "Enter connection string");
|
||||
export const saveChangesInFile = localize('saveChangesInFile', "There are unsaved changes in the current file. Save now?");
|
||||
export const save = localize('save', "Save");
|
||||
export function settingAlreadyExists(settingName: string) { return localize('SettingAlreadyExists', 'Local app setting \'{0}\' already exists. Overwrite?', settingName); }
|
||||
export function failedToParse(errorMessage: string) { return localize('failedToParse', 'Failed to parse "{0}": {1}.', azureFunctionLocalSettingsFileName, errorMessage); }
|
||||
export function jsonParseError(error: string, line: number, column: number) { return localize('jsonParseError', '{0} near line "{1}", column "{2}"', error, line, column); }
|
||||
export const moreInformation = localize('moreInformation', "More Information");
|
||||
export const addPackageReferenceMessage = localize('addPackageReferenceMessage', 'To use SQL bindings, ensure your Azure Functions project has a reference to {0}', sqlExtensionPackageName);
|
||||
export const addSqlBindingPackageError = localize('addSqlBindingPackageError', 'Error adding Sql Binding extension package to project');
|
||||
export const failedToGetConnectionString = localize('failedToGetConnectionString', 'An error occurred generating the connection string for the selected connection');
|
||||
export const connectionProfile = localize('connectionProfile', 'Select a connection profile');
|
||||
export const userConnectionString = localize('userConnectionString', 'Enter connection string');
|
||||
export const selectConnectionString = localize('selectConnectionString', 'Select SQL connection string method');
|
||||
export const selectConnectionError = (err?: any) => err ? localize('selectConnectionError', "Failed to set connection string app setting: {0}", utils.getErrorMessage(err)) : localize('unableToSetConnectionString', "Failed to set connection string app setting");
|
||||
export const includePassword = localize('includePassword', 'Do you want to include the password from this connection in your local.settings.json file?');
|
||||
export const enterPasswordPrompt = localize('enterPasswordPrompt', 'Enter the password to be used for the connection string');
|
||||
export const enterPasswordManually = localize('enterPasswordManually', 'Enter password or press escape to cancel');
|
||||
export const userPasswordLater = localize('userPasswordLater', 'In order to user the SQL connection string later you will need to manually enter the password in your local.settings.json file.');
|
||||
export const openFile = localize('openFile', "Open File");
|
||||
export const closeButton = localize('closeButton', "Close");
|
||||
45
extensions/sql-bindings/src/common/parseJson.ts
Normal file
45
extensions/sql-bindings/src/common/parseJson.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// copied from vscode-azurefunctions extension
|
||||
|
||||
import * as jsonc from 'jsonc-parser';
|
||||
import * as constants from './constants';
|
||||
|
||||
/**
|
||||
* Parses and returns JSON
|
||||
* Has extra logic to remove a BOM character if it exists and handle comments
|
||||
*/
|
||||
export function parseJson<T extends object>(data: string): T {
|
||||
if (data.charCodeAt(0) === 0xFEFF) {
|
||||
data = data.slice(1);
|
||||
}
|
||||
|
||||
const errors: jsonc.ParseError[] = [];
|
||||
const result: T = <T>jsonc.parse(data, errors, { allowTrailingComma: true });
|
||||
if (errors.length > 0) {
|
||||
const [line, column]: [number, number] = getLineAndColumnFromOffset(data, errors[0].offset);
|
||||
throw new Error(constants.jsonParseError(jsonc.printParseErrorCode(errors[0].error), line, column));
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export function getLineAndColumnFromOffset(data: string, offset: number): [number, number] {
|
||||
const lines: string[] = data.split('\n');
|
||||
let charCount: number = 0;
|
||||
let lineCount: number = 0;
|
||||
let column: number = 0;
|
||||
for (const line of lines) {
|
||||
lineCount += 1;
|
||||
const lineLength: number = line.length + 1;
|
||||
charCount += lineLength;
|
||||
if (charCount >= offset) {
|
||||
column = offset - (charCount - lineLength);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return [lineCount, column];
|
||||
}
|
||||
20
extensions/sql-bindings/src/common/telemetry.ts
Normal file
20
extensions/sql-bindings/src/common/telemetry.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 AdsTelemetryReporter from '@microsoft/ads-extension-telemetry';
|
||||
|
||||
const packageInfo = vscode.extensions.getExtension('Microsoft.sql-bindings')?.packageJSON;
|
||||
|
||||
export const TelemetryReporter = new AdsTelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey);
|
||||
|
||||
|
||||
export enum TelemetryViews {
|
||||
SqlBindingsQuickPick = 'SqlBindingsQuickPick'
|
||||
}
|
||||
|
||||
export enum TelemetryActions {
|
||||
startAddSqlBinding = 'startAddSqlBinding',
|
||||
finishAddSqlBinding = 'finishAddSqlBinding'
|
||||
}
|
||||
110
extensions/sql-bindings/src/common/utils.ts
Normal file
110
extensions/sql-bindings/src/common/utils.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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 path from 'path';
|
||||
import * as glob from 'fast-glob';
|
||||
import * as cp from 'child_process';
|
||||
|
||||
export interface ValidationResult {
|
||||
errorMessage: string;
|
||||
validated: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Consolidates on the error message string
|
||||
*/
|
||||
export function getErrorMessage(error: any): string {
|
||||
return (error instanceof Error)
|
||||
? (typeof error.message === 'string' ? error.message : '')
|
||||
: typeof error === 'string' ? error : `${JSON.stringify(error, undefined, '\t')}`;
|
||||
}
|
||||
|
||||
export async function getAzureFunctionService(): Promise<vscodeMssql.IAzureFunctionsService> {
|
||||
if (getAzdataApi()) {
|
||||
// this isn't supported in ADS
|
||||
throw new Error('Azure Functions service is not supported in Azure Data Studio');
|
||||
} else {
|
||||
const api = await getVscodeMssqlApi();
|
||||
return api.azureFunctions;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getVscodeMssqlApi(): Promise<vscodeMssql.IExtension> {
|
||||
const ext = vscode.extensions.getExtension(vscodeMssql.extension.name) as vscode.Extension<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;
|
||||
try {
|
||||
azdataApi = require('azdata');
|
||||
if (!azdataApi?.version) {
|
||||
// webpacking makes the require return an empty object instead of throwing an error so make sure we clear the var
|
||||
azdataApi = undefined;
|
||||
}
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the azdata API if it's available in the context this extension is running in.
|
||||
* @returns The azdata API if it's available
|
||||
*/
|
||||
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) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
if (stderr && stderr.length > 0) {
|
||||
reject(new Error(stderr));
|
||||
return;
|
||||
}
|
||||
resolve(stdout);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the projects of the specified extension in the folder
|
||||
* @param folder
|
||||
* @param projectExtension project extension to filter on
|
||||
* @returns array of project uris
|
||||
*/
|
||||
export async function getAllProjectsInFolder(folder: vscode.Uri, projectExtension: string): Promise<vscode.Uri[]> {
|
||||
// path needs to use forward slashes for glob to work
|
||||
const escapedPath = glob.escapePath(folder.fsPath.replace(/\\/g, '/'));
|
||||
|
||||
// filter for projects with the specified project extension
|
||||
const projFilter = path.posix.join(escapedPath, '**', `*${projectExtension}`);
|
||||
|
||||
// glob will return an array of file paths with forward slashes, so they need to be converted back if on windows
|
||||
return (await glob(projFilter)).map(p => vscode.Uri.file(path.resolve(p)));
|
||||
}
|
||||
Reference in New Issue
Block a user