Remove sql-bindings logic from sql-database-projects (#18754)

* remove sql-bindings logic from sql-database-projects
This commit is contained in:
Vasu Bhog
2022-03-17 11:36:28 -07:00
committed by GitHub
parent bdb81bc9b1
commit 80007a72cc
14 changed files with 2 additions and 1022 deletions

View File

@@ -1,102 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 local.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));
}

View File

@@ -533,48 +533,3 @@ export const defaultDSP = targetPlatformToVersion.get(defaultTargetPlatform)!;
export function getTargetPlatformFromVersion(version: string): string {
return Array.from(targetPlatformToVersion.keys()).filter(k => targetPlatformToVersion.get(k) === version)[0];
}
// 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 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");

View File

@@ -1,45 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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];
}

View File

@@ -16,7 +16,6 @@ export enum TelemetryViews {
ProjectController = 'ProjectController',
SqlProjectPublishDialog = 'SqlProjectPublishDialog',
ProjectTree = 'ProjectTree',
SqlBindingsQuickPick = 'SqlBindingsQuickPick'
}
export enum TelemetryActions {
@@ -33,7 +32,5 @@ export enum TelemetryActions {
build = 'build',
updateProjectForRoundtrip = 'updateProjectForRoundtrip',
changePlatformType = 'changePlatformType',
updateSystemDatabaseReferencesInProjFile = 'updateSystemDatabaseReferencesInProjFile',
startAddSqlBinding = 'startAddSqlBinding',
finishAddSqlBinding = 'finishAddSqlBinding'
updateSystemDatabaseReferencesInProjFile = 'updateSystemDatabaseReferencesInProjFile'
}

View File

@@ -302,16 +302,6 @@ export async function getSchemaCompareService(): Promise<ISchemaCompareService>
}
}
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();
@@ -475,23 +465,6 @@ export async function detectCommandInstallation(command: string): Promise<boolea
return false;
}
/**
* 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)));
}
export function validateSqlServerPortNumber(port: string | undefined): boolean {
if (!port) {
return false;

View File

@@ -16,8 +16,6 @@ import { IconPathHelper } from '../common/iconHelper';
import { WorkspaceTreeItem } from 'dataworkspace';
import * as constants from '../common/constants';
import { SqlDatabaseProjectProvider } from '../projectProvider/projectProvider';
import { launchAddSqlBindingQuickpick } from '../dialogs/addSqlBindingQuickpick';
import { PackageHelper } from '../tools/packageHelper';
import { GenerateProjectFromOpenApiSpecOptions } from 'sqldbproj';
/**
@@ -26,13 +24,11 @@ import { GenerateProjectFromOpenApiSpecOptions } from 'sqldbproj';
export default class MainController implements vscode.Disposable {
protected projectsController: ProjectsController;
protected netcoreTool: NetCoreTool;
protected packageHelper: PackageHelper;
private _outputChannel: vscode.OutputChannel = vscode.window.createOutputChannel(constants.projectsOutputChannel);
public constructor(private context: vscode.ExtensionContext) {
this.projectsController = new ProjectsController(this._outputChannel);
this.netcoreTool = new NetCoreTool(this._outputChannel);
this.packageHelper = new PackageHelper(this._outputChannel);
}
public get extensionContext(): vscode.ExtensionContext {
@@ -89,8 +85,6 @@ export default class MainController implements vscode.Disposable {
vscode.commands.registerCommand('sqlDatabaseProjects.changeTargetPlatform', async (node: WorkspaceTreeItem) => { return this.projectsController.changeTargetPlatform(node); });
vscode.commands.registerCommand('sqlDatabaseProjects.validateExternalStreamingJob', async (node: WorkspaceTreeItem) => { return this.projectsController.validateExternalStreamingJob(node); });
vscode.commands.registerCommand('sqlDatabaseProjects.addSqlBinding', async (uri: vscode.Uri | undefined) => { return launchAddSqlBindingQuickpick(uri, this.packageHelper); });
IconPathHelper.setExtensionContext(this.extensionContext);
await templates.loadTemplates(path.join(this.context.extensionPath, 'resources', 'templates'));

View File

@@ -1,299 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 path from 'path';
import { BindingType, ConnectionDetails, IConnectionInfo } from 'vscode-mssql';
import * as constants from '../common/constants';
import * as utils from '../common/utils';
import * as azureFunctionsUtils from '../common/azureFunctionsUtils';
import { PackageHelper } from '../tools/packageHelper';
import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry';
export async function launchAddSqlBindingQuickpick(uri: vscode.Uri | undefined, packageHelper: PackageHelper): Promise<void> {
TelemetryReporter.sendActionEvent(TelemetryViews.SqlBindingsQuickPick, TelemetryActions.startAddSqlBinding);
const vscodeMssqlApi = await utils.getVscodeMssqlApi();
if (!uri) {
// this command only shows in the command palette when the active editor is a .cs file, so we can safely assume that's the scenario
// when this is called without a uri
uri = vscode.window.activeTextEditor!.document.uri;
if (vscode.window.activeTextEditor!.document.isDirty) {
const result = await vscode.window.showWarningMessage(constants.saveChangesInFile, { modal: true }, constants.save);
if (result !== constants.save) {
return;
}
await vscode.window.activeTextEditor!.document.save();
}
}
// get all the Azure functions in the file
const azureFunctionsService = await utils.getAzureFunctionService();
let getAzureFunctionsResult;
try {
getAzureFunctionsResult = await azureFunctionsService.getAzureFunctions(uri.fsPath);
} catch (e) {
void vscode.window.showErrorMessage(utils.getErrorMessage(e));
return;
}
const azureFunctions = getAzureFunctionsResult.azureFunctions;
if (azureFunctions.length === 0) {
void vscode.window.showErrorMessage(constants.noAzureFunctionsInFile);
return;
}
// 1. select Azure function from the current file
const azureFunctionName = (await vscode.window.showQuickPick(azureFunctions, {
canPickMany: false,
title: constants.selectAzureFunction,
ignoreFocusOut: true
}));
if (!azureFunctionName) {
return;
}
// 2. select input or output binding
const inputOutputItems: (vscode.QuickPickItem & { type: BindingType })[] = [
{
label: constants.input,
type: BindingType.input
},
{
label: constants.output,
type: BindingType.output
}
];
const selectedBinding = (await vscode.window.showQuickPick(inputOutputItems, {
canPickMany: false,
title: constants.selectBindingType,
ignoreFocusOut: true
}));
if (!selectedBinding) {
return;
}
// 3. ask for object name for the binding
const objectName = await vscode.window.showInputBox({
prompt: selectedBinding.type === BindingType.input ? constants.sqlTableOrViewToQuery : constants.sqlTableToUpsert,
placeHolder: constants.placeHolderObject,
validateInput: input => input ? undefined : constants.nameMustNotBeEmpty,
ignoreFocusOut: true
});
if (!objectName) {
return;
}
// 4. ask for connection string setting name
let projectUri: vscode.Uri | undefined;
try {
projectUri = await azureFunctionsUtils.getAFProjectContainingFile(uri);
} catch (e) {
// continue even if there's no AF project found. The binding should still be able to be added as long as there was an azure function found in the file earlier
}
let connectionStringSettingName;
// show the settings from project's local.settings.json if there's an AF functions project
if (projectUri) {
let settings;
try {
settings = await azureFunctionsUtils.getLocalSettingsJson(path.join(path.dirname(projectUri.fsPath!), constants.azureFunctionLocalSettingsFileName));
} catch (e) {
void vscode.window.showErrorMessage(utils.getErrorMessage(e));
return;
}
let existingSettings: (vscode.QuickPickItem)[] = [];
if (settings?.Values) {
existingSettings = Object.keys(settings.Values).map(setting => {
return {
label: setting
} as vscode.QuickPickItem;
});
}
existingSettings.unshift({ label: constants.createNewLocalAppSettingWithIcon });
let sqlConnectionStringSettingExists = existingSettings.find(s => s.label === constants.sqlConnectionStringSetting);
while (!connectionStringSettingName) {
const selectedSetting = await vscode.window.showQuickPick(existingSettings, {
canPickMany: false,
title: constants.selectSetting,
ignoreFocusOut: true
});
if (!selectedSetting) {
// User cancelled
return;
}
if (selectedSetting.label === constants.createNewLocalAppSettingWithIcon) {
const newConnectionStringSettingName = await vscode.window.showInputBox(
{
title: constants.enterConnectionStringSettingName,
ignoreFocusOut: true,
value: sqlConnectionStringSettingExists ? '' : constants.sqlConnectionStringSetting,
validateInput: input => input ? undefined : constants.nameMustNotBeEmpty
}
) ?? '';
if (!newConnectionStringSettingName) {
// go back to select setting quickpick if user escapes from inputting the setting name in case they changed their mind
continue;
}
// show the connection string methods (user input and connection profile options)
const listOfConnectionStringMethods = [constants.connectionProfile, constants.userConnectionString];
while (true) {
const selectedConnectionStringMethod = await vscode.window.showQuickPick(listOfConnectionStringMethods, {
canPickMany: false,
title: constants.selectConnectionString,
ignoreFocusOut: true
});
if (!selectedConnectionStringMethod) {
// User cancelled
return;
}
let connectionString: string = '';
let includePassword: string | undefined;
let connectionInfo: IConnectionInfo | undefined;
let connectionDetails: ConnectionDetails;
if (selectedConnectionStringMethod === constants.userConnectionString) {
// User chooses to enter connection string manually
connectionString = await vscode.window.showInputBox(
{
title: constants.enterConnectionString,
ignoreFocusOut: true,
value: 'Server=localhost;Initial Catalog={db_name};User ID=sa;Password={your_password};Persist Security Info=False',
validateInput: input => input ? undefined : constants.valueMustNotBeEmpty
}
) ?? '';
} else {
// Let user choose from existing connections to create connection string from
connectionInfo = await vscodeMssqlApi.promptForConnection(true);
if (!connectionInfo) {
// User cancelled return to selectedConnectionStringMethod prompt
continue;
}
connectionDetails = { options: connectionInfo };
try {
// Prompt to include password in connection string if authentication type is SqlLogin and connection has password saved
if (connectionInfo.authenticationType === 'SqlLogin' && connectionInfo.password) {
includePassword = await vscode.window.showQuickPick([constants.yesString, constants.noString], {
title: constants.includePassword,
canPickMany: false,
ignoreFocusOut: true
});
if (includePassword === constants.yesString) {
// set connection string to include password
connectionString = await vscodeMssqlApi.getConnectionString(connectionDetails, true, false);
}
}
// set connection string to not include the password if connection info does not include password, or user chooses to not include password, or authentication type is not sql login
if (includePassword !== constants.yesString) {
connectionString = await vscodeMssqlApi.getConnectionString(connectionDetails, false, false);
}
} catch (e) {
// failed to get connection string for selected connection and will go back to prompt for connection string methods
console.warn(e);
void vscode.window.showErrorMessage(constants.failedToGetConnectionString);
continue;
}
}
if (connectionString) {
try {
const projectFolder: string = path.dirname(projectUri.fsPath);
const localSettingsPath: string = path.join(projectFolder, constants.azureFunctionLocalSettingsFileName);
let userPassword: string | undefined;
// Ask user to enter password if auth type is sql login and password is not saved
if (connectionInfo?.authenticationType === 'SqlLogin' && !connectionInfo?.password) {
userPassword = await vscode.window.showInputBox({
prompt: constants.enterPasswordPrompt,
placeHolder: constants.enterPasswordManually,
ignoreFocusOut: true,
password: true,
validateInput: input => input ? undefined : constants.valueMustNotBeEmpty
});
if (userPassword) {
// if user enters password replace password placeholder with user entered password
connectionString = connectionString.replace(constants.passwordPlaceholder, userPassword);
}
}
if (includePassword !== constants.yesString && !userPassword && connectionInfo?.authenticationType === 'SqlLogin') {
// if user does not want to include password or user does not enter password, show warning message that they will have to enter it manually later in local.settings.json
void vscode.window.showWarningMessage(constants.userPasswordLater, constants.openFile, constants.closeButton).then(async (result) => {
if (result === constants.openFile) {
// open local.settings.json file
void vscode.commands.executeCommand(constants.vscodeOpenCommand, vscode.Uri.file(localSettingsPath));
}
});
}
const success = await azureFunctionsUtils.setLocalAppSetting(projectFolder, newConnectionStringSettingName, connectionString);
if (success) {
// exit both loops and insert binding
connectionStringSettingName = newConnectionStringSettingName;
break;
} else {
void vscode.window.showErrorMessage(constants.selectConnectionError());
}
} catch (e) {
// display error message and show select setting quickpick again
void vscode.window.showErrorMessage(constants.selectConnectionError(e));
continue;
}
}
}
} else {
// If user cancels out of this or doesn't want to overwrite an existing setting
// just return them to the select setting quickpick in case they changed their mind
connectionStringSettingName = selectedSetting.label;
}
}
} else {
// if no AF project was found or there's more than one AF functions project in the workspace,
// ask for the user to input the setting name
connectionStringSettingName = await vscode.window.showInputBox({
prompt: constants.connectionStringSetting,
placeHolder: constants.connectionStringSettingPlaceholder,
ignoreFocusOut: true
});
}
if (!connectionStringSettingName) {
return;
}
// 5. insert binding
try {
const result = await azureFunctionsService.addSqlBinding(selectedBinding.type, uri.fsPath, azureFunctionName, objectName, connectionStringSettingName);
if (!result.success) {
void vscode.window.showErrorMessage(result.errorMessage);
TelemetryReporter.sendErrorEvent(TelemetryViews.SqlBindingsQuickPick, TelemetryActions.finishAddSqlBinding);
return;
}
TelemetryReporter.createActionEvent(TelemetryViews.SqlBindingsQuickPick, TelemetryActions.finishAddSqlBinding)
.withAdditionalProperties({ bindingType: selectedBinding.label })
.send();
} catch (e) {
void vscode.window.showErrorMessage(utils.getErrorMessage(e));
TelemetryReporter.sendErrorEvent(TelemetryViews.SqlBindingsQuickPick, TelemetryActions.finishAddSqlBinding);
return;
}
// 6. Add sql extension package reference to project. If the reference is already there, it doesn't get added again
await packageHelper.addPackageToAFProjectContainingFile(uri, constants.sqlExtensionPackageName);
}

View File

@@ -1,134 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as should from 'should';
import * as sinon from 'sinon';
import * as vscode from 'vscode';
import * as TypeMoq from 'typemoq';
import * as utils from '../../common/utils';
import * as constants from '../../common/constants';
import * as azureFunctionUtils from '../../common/azureFunctionsUtils';
import { createContext, TestContext, createTestCredentials } from '../testContext';
import { launchAddSqlBindingQuickpick } from '../../dialogs/addSqlBindingQuickpick';
import { PackageHelper } from '../../tools/packageHelper';
let testContext: TestContext;
let packageHelper: PackageHelper;
describe('Add SQL Binding quick pick', () => {
beforeEach(function (): void {
testContext = createContext();
packageHelper = new PackageHelper(testContext.outputChannel);
});
afterEach(function (): void {
sinon.restore();
});
it('Should show error if the file contains no Azure Functions', async function (): Promise<void> {
sinon.stub(utils, 'getAzureFunctionService').resolves(testContext.azureFunctionService.object);
sinon.stub(utils, 'getVscodeMssqlApi').resolves(testContext.vscodeMssqlIExtension.object);
const spy = sinon.spy(vscode.window, 'showErrorMessage');
testContext.azureFunctionService.setup(x => x.getAzureFunctions(TypeMoq.It.isAny())).returns(async () => {
return Promise.resolve({
success: true,
errorMessage: '',
azureFunctions: []
});
});
await launchAddSqlBindingQuickpick(vscode.Uri.file('testUri'), packageHelper);
const msg = constants.noAzureFunctionsInFile;
should(spy.calledOnce).be.true('showErrorMessage should have been called exactly once');
should(spy.calledWith(msg)).be.true(`showErrorMessage not called with expected message '${msg}' Actual '${spy.getCall(0).args[0]}'`);
});
it('Should show error if adding SQL binding was not successful', async function (): Promise<void> {
sinon.stub(utils, 'getAzureFunctionService').resolves(testContext.azureFunctionService.object);
sinon.stub(utils, 'getVscodeMssqlApi').resolves(testContext.vscodeMssqlIExtension.object);
const spy = sinon.spy(vscode.window, 'showErrorMessage');
testContext.azureFunctionService.setup(x => x.getAzureFunctions(TypeMoq.It.isAny())).returns(async () => {
return Promise.resolve({
success: true,
errorMessage: '',
azureFunctions: ['af1', 'af2']
});
});
//failure since no AFs are found in the project
sinon.stub(azureFunctionUtils, 'getAFProjectContainingFile').resolves(undefined);
const errormsg = 'Error inserting binding';
testContext.azureFunctionService.setup(x => x.addSqlBinding(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(async () => {
return Promise.resolve({
success: false,
errorMessage: errormsg
});
});
// select Azure function
let quickpickStub = sinon.stub(vscode.window, 'showQuickPick').onFirstCall().resolves({ label: 'af1' });
// select input or output binding
quickpickStub.onSecondCall().resolves({ label: constants.input });
// give object name
let inputBoxStub = sinon.stub(vscode.window, 'showInputBox').onFirstCall().resolves('dbo.table1');
// give connection string setting name
inputBoxStub.onSecondCall().resolves('sqlConnectionString');
await launchAddSqlBindingQuickpick(vscode.Uri.file('testUri'), packageHelper);
should(spy.calledOnce).be.true('showErrorMessage should have been called exactly once');
should(spy.calledWith(errormsg)).be.true(`showErrorMessage not called with expected message '${errormsg}' Actual '${spy.getCall(0).args[0]}'`);
});
it('Should show error connection profile does not connect', async function (): Promise<void> {
sinon.stub(utils, 'getAzureFunctionService').resolves(testContext.azureFunctionService.object);
sinon.stub(utils, 'getVscodeMssqlApi').resolves(testContext.vscodeMssqlIExtension.object);
let connectionCreds = createTestCredentials();
sinon.stub(azureFunctionUtils, 'getAFProjectContainingFile').resolves(vscode.Uri.file('testUri'));
testContext.azureFunctionService.setup(x => x.getAzureFunctions(TypeMoq.It.isAny())).returns(async () => {
return Promise.resolve({
success: true,
errorMessage: '',
azureFunctions: ['af1']
});
});
// Mocks connect call to mssql
let error = new Error('Connection Request Failed');
testContext.vscodeMssqlIExtension.setup(x => x.connect(TypeMoq.It.isAny(), undefined)).throws(error);
// Mocks promptForConnection
testContext.vscodeMssqlIExtension.setup(x => x.promptForConnection(true)).returns(() => Promise.resolve(connectionCreds));
let quickpickStub = sinon.stub(vscode.window, 'showQuickPick');
// select Azure function
quickpickStub.onFirstCall().resolves({ label: 'af1' });
// select input or output binding
quickpickStub.onSecondCall().resolves({ label: constants.input });
// give object name
let inputBoxStub = sinon.stub(vscode.window, 'showInputBox').onFirstCall().resolves('dbo.table1');
// select connection profile
quickpickStub.onThirdCall().resolves({ label: constants.createNewLocalAppSettingWithIcon });
// give connection string setting name
inputBoxStub.onSecondCall().resolves('SqlConnectionString');
// select connection profile method
quickpickStub.onCall(3).resolves({ label: constants.connectionProfile });
await launchAddSqlBindingQuickpick(vscode.Uri.file('testUri'), packageHelper);
// should go back to the select connection string methods
should(quickpickStub.callCount === 5);
should(quickpickStub.getCall(4).args).deepEqual([
[constants.connectionProfile, constants.userConnectionString],
{
canPickMany: false,
ignoreFocusOut: true,
title: constants.selectConnectionString
}]);
});
});

View File

@@ -1,62 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as should from 'should';
import * as os from 'os';
import * as vscode from 'vscode';
import * as sinon from 'sinon';
import * as constants from '../common/constants';
import * as azureFunctionUtils from '../common/azureFunctionsUtils';
import { PackageHelper } from '../tools/packageHelper';
import { createContext, TestContext } from './testContext';
let testContext: TestContext;
let packageHelper: PackageHelper;
describe('PackageHelper tests', function (): void {
beforeEach(function (): void {
testContext = createContext();
packageHelper = new PackageHelper(testContext.outputChannel);
});
afterEach(function (): void {
sinon.restore();
});
it('Should construct correct add Package Arguments', function (): void {
const packageHelper = new PackageHelper( vscode.window.createOutputChannel('db project test'));
const projectUri = vscode.Uri.file('dummy\\project\\path.csproj');
const result = packageHelper.constructAddPackageArguments(projectUri, constants.sqlExtensionPackageName);
if (os.platform() === 'win32') {
should(result).equal(` add "\\\\dummy\\\\project\\\\path.csproj" package ${constants.sqlExtensionPackageName} --prerelease`);
}
else {
should(result).equal(` add "/dummy/project/path.csproj" package ${constants.sqlExtensionPackageName} --prerelease`);
}
});
it('Should construct correct add Package Arguments with version', function (): void {
const packageHelper = new PackageHelper( vscode.window.createOutputChannel('db project test'));
const projectUri = vscode.Uri.file('dummy\\project\\path.csproj');
const result = packageHelper.constructAddPackageArguments(projectUri, constants.sqlExtensionPackageName, constants.VersionNumber);
if (os.platform() === 'win32') {
should(result).equal(` add "\\\\dummy\\\\project\\\\path.csproj" package ${constants.sqlExtensionPackageName} -v ${constants.VersionNumber}`);
}
else {
should(result).equal(` add "/dummy/project/path.csproj" package ${constants.sqlExtensionPackageName} -v ${constants.VersionNumber}`);
}
});
it('Should show info message to add sql bindings package if project is not found', async function (): Promise<void> {
sinon.stub(azureFunctionUtils, 'getAFProjectContainingFile').resolves(undefined);
const spy = sinon.spy(vscode.window, 'showInformationMessage');
await packageHelper.addPackageToAFProjectContainingFile(vscode.Uri.file(''), constants.sqlExtensionPackageName);
should(spy.calledOnce).be.true('showInformationMessage should have been called exactly once');
});
});

View File

@@ -8,16 +8,11 @@ import * as azdata from 'azdata';
import * as path from 'path';
import * as TypeMoq from 'typemoq';
import * as mssql from '../../../mssql/src/mssql';
import * as vscodeMssql from 'vscode-mssql';
export interface TestContext {
context: vscode.ExtensionContext;
dacFxService: TypeMoq.IMock<mssql.IDacFxService>;
azureFunctionService: TypeMoq.IMock<vscodeMssql.IAzureFunctionsService>;
outputChannel: vscode.OutputChannel;
vscodeMssqlIExtension: TypeMoq.IMock<vscodeMssql.IExtension>
dacFxMssqlService: TypeMoq.IMock<vscodeMssql.IDacFxService>;
schemaCompareService: TypeMoq.IMock<vscodeMssql.ISchemaCompareService>;
}
export const mockDacFxResult = {
@@ -124,153 +119,6 @@ export class MockDacFxService implements mssql.IDacFxService {
public validateStreamingJob(_: string, __: string): Thenable<mssql.ValidateStreamingJobResult> { return Promise.resolve(mockDacFxResult); }
}
export const mockResultStatus = {
success: true,
errorMessage: ''
};
export const mockGetAzureFunctionsResult = {
success: true,
errorMessage: '',
azureFunctions: []
};
export class MockAzureFunctionService implements vscodeMssql.IAzureFunctionsService {
addSqlBinding(_: vscodeMssql.BindingType, __: string, ___: string, ____: string, _____: string): Thenable<vscodeMssql.ResultStatus> { return Promise.resolve(mockResultStatus); }
getAzureFunctions(_: string): Thenable<vscodeMssql.GetAzureFunctionsResult> { return Promise.resolve(mockGetAzureFunctionsResult); }
}
export const mockDacFxMssqlOptionResult: vscodeMssql.DacFxOptionsResult = {
success: true,
errorMessage: '',
deploymentOptions: {
ignoreTableOptions: false,
ignoreSemicolonBetweenStatements: false,
ignoreRouteLifetime: false,
ignoreRoleMembership: false,
ignoreQuotedIdentifiers: false,
ignorePermissions: false,
ignorePartitionSchemes: false,
ignoreObjectPlacementOnPartitionScheme: false,
ignoreNotForReplication: false,
ignoreLoginSids: false,
ignoreLockHintsOnIndexes: false,
ignoreKeywordCasing: false,
ignoreIndexPadding: false,
ignoreIndexOptions: false,
ignoreIncrement: false,
ignoreIdentitySeed: false,
ignoreUserSettingsObjects: false,
ignoreFullTextCatalogFilePath: false,
ignoreWhitespace: false,
ignoreWithNocheckOnForeignKeys: false,
verifyCollationCompatibility: false,
unmodifiableObjectWarnings: false,
treatVerificationErrorsAsWarnings: false,
scriptRefreshModule: false,
scriptNewConstraintValidation: false,
scriptFileSize: false,
scriptDeployStateChecks: false,
scriptDatabaseOptions: false,
scriptDatabaseCompatibility: false,
scriptDatabaseCollation: false,
runDeploymentPlanExecutors: false,
registerDataTierApplication: false,
populateFilesOnFileGroups: false,
noAlterStatementsToChangeClrTypes: false,
includeTransactionalScripts: false,
includeCompositeObjects: false,
allowUnsafeRowLevelSecurityDataMovement: false,
ignoreWithNocheckOnCheckConstraints: false,
ignoreFillFactor: false,
ignoreFileSize: false,
ignoreFilegroupPlacement: false,
doNotAlterReplicatedObjects: false,
doNotAlterChangeDataCaptureObjects: false,
disableAndReenableDdlTriggers: false,
deployDatabaseInSingleUserMode: false,
createNewDatabase: false,
compareUsingTargetCollation: false,
commentOutSetVarDeclarations: false,
blockWhenDriftDetected: false,
blockOnPossibleDataLoss: false,
backupDatabaseBeforeChanges: false,
allowIncompatiblePlatform: false,
allowDropBlockingAssemblies: false,
dropConstraintsNotInSource: false,
dropDmlTriggersNotInSource: false,
dropExtendedPropertiesNotInSource: false,
dropIndexesNotInSource: false,
ignoreFileAndLogFilePath: false,
ignoreExtendedProperties: false,
ignoreDmlTriggerState: false,
ignoreDmlTriggerOrder: false,
ignoreDefaultSchema: false,
ignoreDdlTriggerState: false,
ignoreDdlTriggerOrder: false,
ignoreCryptographicProviderFilePath: false,
verifyDeployment: false,
ignoreComments: false,
ignoreColumnCollation: false,
ignoreAuthorizer: false,
ignoreAnsiNulls: false,
generateSmartDefaults: false,
dropStatisticsNotInSource: false,
dropRoleMembersNotInSource: false,
dropPermissionsNotInSource: false,
dropObjectsNotInSource: false,
ignoreColumnOrder: false,
doNotDropObjectTypes: [],
excludeObjectTypes: []
}
};
export class MockDacFxMssqlService implements vscodeMssql.IDacFxService {
public exportBacpac(_: string, __: string, ___: string, ____: vscodeMssql.TaskExecutionMode): Thenable<vscodeMssql.DacFxResult> { return Promise.resolve(mockDacFxResult); }
public importBacpac(_: string, __: string, ___: string, ____: vscodeMssql.TaskExecutionMode): Thenable<mssql.DacFxResult> { return Promise.resolve(mockDacFxResult); }
public extractDacpac(_: string, __: string, ___: string, ____: string, _____: string, ______: vscodeMssql.TaskExecutionMode): Thenable<mssql.DacFxResult> { return Promise.resolve(mockDacFxResult); }
public createProjectFromDatabase(_: string, __: string, ___: string, ____: string, _____: string, ______: vscodeMssql.ExtractTarget, _______: vscodeMssql.TaskExecutionMode): Thenable<vscodeMssql.DacFxResult> { return Promise.resolve(mockDacFxResult); }
public deployDacpac(_: string, __: string, ___: boolean, ____: string, _____: vscodeMssql.TaskExecutionMode, ______?: Record<string, string>): Thenable<mssql.DacFxResult> { return Promise.resolve(mockDacFxResult); }
public generateDeployScript(_: string, __: string, ___: string, ____: vscodeMssql.TaskExecutionMode, ______?: Record<string, string>): Thenable<mssql.DacFxResult> { return Promise.resolve(mockDacFxResult); }
public generateDeployPlan(_: string, __: string, ___: string, ____: vscodeMssql.TaskExecutionMode): Thenable<vscodeMssql.GenerateDeployPlanResult> { return Promise.resolve(mockDacFxResult); }
public getOptionsFromProfile(_: string): Thenable<vscodeMssql.DacFxOptionsResult> { return Promise.resolve(mockDacFxMssqlOptionResult); }
public validateStreamingJob(_: string, __: string): Thenable<mssql.ValidateStreamingJobResult> { return Promise.resolve(mockDacFxResult); }
}
export class MockSchemaCompareService implements vscodeMssql.ISchemaCompareService {
schemaCompareGetDefaultOptions(): Thenable<vscodeMssql.SchemaCompareOptionsResult> {
throw new Error('Method not implemented.');
}
}
export class MockVscodeMssqlIExtension implements vscodeMssql.IExtension {
sqlToolsServicePath: string = '';
dacFx: vscodeMssql.IDacFxService;
schemaCompare: vscodeMssql.ISchemaCompareService;
azureFunctions: vscodeMssql.IAzureFunctionsService;
constructor() {
this.dacFx = new MockDacFxMssqlService;
this.schemaCompare = new MockSchemaCompareService;
this.azureFunctions = new MockAzureFunctionService;
}
promptForConnection(_?: boolean): Promise<vscodeMssql.IConnectionInfo | undefined> {
throw new Error('Method not implemented.');
}
connect(_: vscodeMssql.IConnectionInfo, __?: boolean): Promise<string> {
throw new Error('Method not implemented.');
}
listDatabases(_: string): Promise<string[]> {
throw new Error('Method not implemented.');
}
getDatabaseNameFromTreeNode(_: vscodeMssql.ITreeNodeInfo): string {
throw new Error('Method not implemented.');
}
getConnectionString(_: string | vscodeMssql.ConnectionDetails, ___?: boolean, _____?: boolean): Promise<string> {
throw new Error('Method not implemented.');
}
}
export function createContext(): TestContext {
let extensionPath = path.join(__dirname, '..', '..');
@@ -303,10 +151,6 @@ export function createContext(): TestContext {
extension: undefined as any
},
dacFxService: TypeMoq.Mock.ofType(MockDacFxService),
azureFunctionService: TypeMoq.Mock.ofType(MockAzureFunctionService),
vscodeMssqlIExtension: TypeMoq.Mock.ofType(MockVscodeMssqlIExtension),
dacFxMssqlService: TypeMoq.Mock.ofType(MockDacFxMssqlService),
schemaCompareService: TypeMoq.Mock.ofType(MockSchemaCompareService),
outputChannel: {
name: '',
append: () => { },
@@ -342,41 +186,3 @@ export const mockConnectionProfile: azdata.IConnectionProfile = {
connectionName: 'My Connection Name'
}
};
export function createTestCredentials(): vscodeMssql.IConnectionInfo {
const creds: vscodeMssql.IConnectionInfo = {
server: 'my-server',
database: 'my_db',
user: 'sa',
password: '12345678',
email: 'test-email',
accountId: 'test-account-id',
port: 1234,
authenticationType: 'test',
azureAccountToken: '',
expiresOn: 0,
encrypt: false,
trustServerCertificate: false,
persistSecurityInfo: false,
connectTimeout: 15,
connectRetryCount: 0,
connectRetryInterval: 0,
applicationName: 'vscode-mssql',
workstationId: 'test',
applicationIntent: '',
currentLanguage: '',
pooling: true,
maxPoolSize: 15,
minPoolSize: 0,
loadBalanceTimeout: 0,
replication: false,
attachDbFilename: '',
failoverPartner: '',
multiSubnetFailover: false,
multipleActiveResultSets: false,
packetSize: 8192,
typeSystemVersion: 'Latest',
connectionString: ''
};
return creds;
}

View File

@@ -1,84 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 utils from '../common/utils';
import * as azureFunctionsUtils from '../common/azureFunctionsUtils';
import * as constants from '../common/constants';
import { NetCoreTool } from './netcoreTool';
import { ShellCommandOptions } from './shellExecutionHelper';
export class PackageHelper {
private netCoreTool: NetCoreTool;
private _outputChannel: vscode.OutputChannel;
constructor(outputChannel: vscode.OutputChannel) {
this.netCoreTool = new NetCoreTool(outputChannel);
this._outputChannel = outputChannel;
}
/**
* Constructs the parameters for a dotnet add package
* @param projectUri uri of project to add package to
* @param packageName name of package
* @param packageVersion optional version of package. If none, latest will be pulled in
* @returns string constructed with the arguments for dotnet add package
*/
public constructAddPackageArguments(projectUri: vscode.Uri, packageName: string, packageVersion?: string): string {
const projectPath = utils.getQuotedPath(projectUri.fsPath);
if (packageVersion) {
return ` add ${projectPath} package ${packageName} -v ${packageVersion}`;
} else {
// pull in the latest version of the package and allow prerelease versions
return ` add ${projectPath} package ${packageName} --prerelease`;
}
}
/**
* Runs dotnet add package to add a package reference to the specified project. If the project already has a package reference
* for this package version, the project file won't get updated
* @param projectUri uri of project to add package to
* @param packageName name of package
* @param packageVersion optional version of package. If none, latest will be pulled in
*/
public async addPackage(projectUri: vscode.Uri, packageName: string, packageVersion?: string): Promise<void> {
const addOptions: ShellCommandOptions = {
commandTitle: constants.addPackage,
argument: this.constructAddPackageArguments(projectUri, packageName, packageVersion)
};
// Add package can be done with any version of .NET so skip the supported version check
await this.netCoreTool.runDotnetCommand(addOptions, true);
}
/**
* Adds specified package to Azure Functions project the specified file is a part of
* @param fileUri uri of file to find the containing AF project of to add package reference to
* @param packageName package to add reference to
* @param packageVersion optional version of package. If none, latest will be pulled in
*/
public async addPackageToAFProjectContainingFile(fileUri: vscode.Uri, packageName: string, packageVersion?: string): Promise<void> {
try {
const project = await azureFunctionsUtils.getAFProjectContainingFile(fileUri);
// if no AF projects were found, an error gets thrown from getAFProjectContainingFile(). This check is temporary until
// multiple AF projects in the workspace is handled. That scenario returns undefined and shows an info message telling the
// user to make sure their project has the package reference
if (project) {
await this.addPackage(project, packageName, packageVersion);
} else {
void vscode.window.showInformationMessage(constants.addPackageReferenceMessage, constants.moreInformation).then((result) => {
if (result === constants.moreInformation) {
void vscode.env.openExternal(vscode.Uri.parse(constants.sqlBindingsHelpLink));
}
});
}
} catch (e) {
const result = await vscode.window.showErrorMessage(constants.addSqlBindingPackageError, constants.checkoutOutputMessage);
if (result === constants.checkoutOutputMessage) {
this._outputChannel.show();
}
}
}
}