diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index 01390619bd..39ae35b3de 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -444,7 +444,9 @@ export function getTargetPlatformFromVersion(version: string): string { // Insert SQL binding export const hostFileName = 'host.json'; +export const sqlExtensionPackageName = 'Microsoft.Azure.WebJobs.Extensions.Sql'; export const placeHolderObject = '[dbo].[table1]'; + export const input = localize('input', "Input"); export const output = localize('output', "Output"); export const selectBindingType = localize('selectBindingType', "Select type of binding"); @@ -454,3 +456,5 @@ export const sqlTableToUpsert = localize('sqlTableToUpsert', "SQL table to upser export const connectionStringSetting = localize('connectionStringSetting', "Connection string setting name"); 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"); diff --git a/extensions/sql-database-projects/src/common/utils.ts b/extensions/sql-database-projects/src/common/utils.ts index fab38074bd..bd31d14c51 100644 --- a/extensions/sql-database-projects/src/common/utils.ts +++ b/extensions/sql-database-projects/src/common/utils.ts @@ -488,3 +488,20 @@ export async function retry( return undefined; } + +/** + * 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 { + // 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))); +} diff --git a/extensions/sql-database-projects/src/controllers/mainController.ts b/extensions/sql-database-projects/src/controllers/mainController.ts index d9e8ae7467..ccad7476e2 100644 --- a/extensions/sql-database-projects/src/controllers/mainController.ts +++ b/extensions/sql-database-projects/src/controllers/mainController.ts @@ -16,6 +16,7 @@ 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'; /** * The main controller class that initializes the extension @@ -23,11 +24,13 @@ import { launchAddSqlBindingQuickpick } from '../dialogs/addSqlBindingQuickpick' 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 { @@ -74,7 +77,7 @@ export default class MainController implements vscode.Disposable { vscode.commands.registerCommand('sqlDatabaseProjects.changeTargetPlatform', async (node: WorkspaceTreeItem) => { await this.projectsController.changeTargetPlatform(node); }); vscode.commands.registerCommand('sqlDatabaseProjects.validateExternalStreamingJob', async (node: WorkspaceTreeItem) => { await this.projectsController.validateExternalStreamingJob(node); }); - vscode.commands.registerCommand('sqlDatabaseProjects.addSqlBinding', async (uri: vscode.Uri | undefined) => { await launchAddSqlBindingQuickpick(uri); }); + vscode.commands.registerCommand('sqlDatabaseProjects.addSqlBinding', async (uri: vscode.Uri | undefined) => { await launchAddSqlBindingQuickpick(uri, this.packageHelper); }); IconPathHelper.setExtensionContext(this.extensionContext); diff --git a/extensions/sql-database-projects/src/dialogs/addSqlBindingQuickpick.ts b/extensions/sql-database-projects/src/dialogs/addSqlBindingQuickpick.ts index 54914c4ea6..adeba93085 100644 --- a/extensions/sql-database-projects/src/dialogs/addSqlBindingQuickpick.ts +++ b/extensions/sql-database-projects/src/dialogs/addSqlBindingQuickpick.ts @@ -2,8 +2,9 @@ import * as vscode from 'vscode'; import { BindingType } from 'vscode-mssql'; import * as constants from '../common/constants'; import * as utils from '../common/utils'; +import { PackageHelper } from '../tools/packageHelper'; -export async function launchAddSqlBindingQuickpick(uri: vscode.Uri | undefined): Promise { +export async function launchAddSqlBindingQuickpick(uri: vscode.Uri | undefined, packageHelper: PackageHelper): Promise { 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 @@ -95,5 +96,8 @@ export async function launchAddSqlBindingQuickpick(uri: vscode.Uri | undefined): vscode.window.showErrorMessage(e); 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.fsPath, constants.sqlExtensionPackageName); } diff --git a/extensions/sql-database-projects/src/tools/packageHelper.ts b/extensions/sql-database-projects/src/tools/packageHelper.ts new file mode 100644 index 0000000000..390adc92a7 --- /dev/null +++ b/extensions/sql-database-projects/src/tools/packageHelper.ts @@ -0,0 +1,104 @@ +/*--------------------------------------------------------------------------------------------- + * 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 '../common/utils'; +import * as constants from '../common/constants'; +import { DotNetCommandOptions, NetCoreTool } from './netcoreTool'; + +export class PackageHelper { + private netCoreTool: NetCoreTool; + + constructor(outputChannel: vscode.OutputChannel) { + this.netCoreTool = new NetCoreTool(outputChannel); + } + + /** + * Constructs the parameters for a dotnet add package + * @param projectPath full path to 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(projectPath: string, packageName: string, packageVersion?: string): string { + projectPath = utils.getQuotedPath(projectPath); + 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 projectPath full path to 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(project: string, packageName: string, packageVersion?: string): Promise { + const addOptions: DotNetCommandOptions = { + commandTitle: constants.addPackage, + argument: this.constructAddPackageArguments(project, packageName, packageVersion) + }; + + await this.netCoreTool.runDotnetCommand(addOptions); + } + + /** + * Adds specified package to Azure Functions project the specified file is a part of + * @param filePath full path to 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(filePath: string, packageName: string, packageVersion?: string): Promise { + try { + const project = await this.getAFProjectContainingFile(filePath); + + // 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); + } + } catch (e) { + vscode.window.showErrorMessage(e.message); + } + } + + /** + * Gets the Azure Functions project that contains the given file + * @param filePath file that the containing project needs to be found for + * @returns filepath of project or undefined if project couldn't be found + */ + public async getAFProjectContainingFile(filePath: string): Promise { + // 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 => this.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 + vscode.window.showInformationMessage(`To use SQL bindings, ensure your Azure Functions project has a reference to ${constants.sqlExtensionPackageName}`); + console.error('need to find which project contains the file ' + filePath); + return undefined; + } else if (functionsProjects.length === 0) { + throw new Error(constants.noAzureFunctionsProjectsInWorkspace); + } else { + return functionsProjects[0].fsPath; + } + } + + // Use 'host.json' as an indicator that this is a functions project + // copied from verifyIsproject.ts in vscode-azurefunctions extension + async isFunctionProject(folderPath: string): Promise { + return await fse.pathExists(path.join(folderPath, constants.hostFileName)); + } +} +