diff --git a/extensions/sql-database-projects/package.json b/extensions/sql-database-projects/package.json index a96a6008db..cd45c8f844 100644 --- a/extensions/sql-database-projects/package.json +++ b/extensions/sql-database-projects/package.json @@ -399,13 +399,14 @@ "@types/xml-formatter": "^1.1.0", "@microsoft/ads-extension-telemetry": "^1.1.5", "fast-glob": "^3.1.0", + "fs-extra": "^5.0.0", + "generate-password": "^1.6.0", + "jsonc-parser": "^2.3.1", "promisify-child-process": "^3.1.1", "vscode-languageclient": "^5.3.0-next.1", "vscode-nls": "^4.1.2", "xml-formatter": "^2.1.0", - "xmldom": "^0.3.0", - "generate-password": "^1.6.0", - "fs-extra": "^5.0.0" + "xmldom": "^0.3.0" }, "devDependencies": { "@types/mocha": "^5.2.5", diff --git a/extensions/sql-database-projects/src/common/azureFunctionsUtils.ts b/extensions/sql-database-projects/src/common/azureFunctionsUtils.ts new file mode 100644 index 0000000000..e18591bd26 --- /dev/null +++ b/extensions/sql-database-projects/src/common/azureFunctionsUtils.ts @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { + 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 + }; +} + +/** + * Gets the Azure Functions project that contains the given file if the project is open in one of the workspace folders + * @param filePath file that the containing project needs to be found for + * @returns filepath of project or undefined if project couldn't be found + */ +export async function 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 => 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 ' + 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 +export async function isFunctionProject(folderPath: string): Promise { + return fse.pathExists(path.join(folderPath, constants.hostFileName)); +} diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index aab3bf0114..75ad6173ac 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -7,7 +7,7 @@ import * as nls from 'vscode-nls'; import { SqlTargetPlatform } from 'sqldbproj'; import * as utils from '../common/utils'; -const localize = nls.loadMessageBundle(); +export const localize = nls.loadMessageBundle(); // Placeholder values export const dataSourcesFileName = 'datasources.json'; @@ -455,7 +455,10 @@ export const selectAzureFunction = localize('selectAzureFunction', "Select an Az export const sqlObjectToQuery = localize('sqlObjectToQuery', "SQL object 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 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); } diff --git a/extensions/sql-database-projects/src/common/parseJson.ts b/extensions/sql-database-projects/src/common/parseJson.ts new file mode 100644 index 0000000000..c474ac41e2 --- /dev/null +++ b/extensions/sql-database-projects/src/common/parseJson.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.md 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(data: string): T { + if (data.charCodeAt(0) === 0xFEFF) { + data = data.slice(1); + } + + const errors: jsonc.ParseError[] = []; + const result: 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]; +} diff --git a/extensions/sql-database-projects/src/dialogs/addSqlBindingQuickpick.ts b/extensions/sql-database-projects/src/dialogs/addSqlBindingQuickpick.ts index 14f35bafee..4e1970b7a0 100644 --- a/extensions/sql-database-projects/src/dialogs/addSqlBindingQuickpick.ts +++ b/extensions/sql-database-projects/src/dialogs/addSqlBindingQuickpick.ts @@ -1,7 +1,9 @@ import * as vscode from 'vscode'; +import * as path from 'path'; import { BindingType } 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'; export async function launchAddSqlBindingQuickpick(uri: vscode.Uri | undefined, packageHelper: PackageHelper): Promise { @@ -73,20 +75,43 @@ export async function launchAddSqlBindingQuickpick(uri: vscode.Uri | undefined, } // 4. ask for connection string setting name - // TODO: load local settings from local.settings.json like in LocalAppSettingListStep in vscode-azurefunctions repo - const connectionStringSetting = await vscode.window.showInputBox({ - prompt: constants.connectionStringSetting, - placeHolder: constants.connectionStringSettingPlaceholder, - ignoreFocusOut: true - }); + let project: string | undefined; + try { + project = await azureFunctionsUtils.getAFProjectContainingFile(uri.fsPath); + } 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 + } - if (!connectionStringSetting) { + let connectionStringSettingName; + + // show the settings from project's local.settings.json if there's an AF functions project + // TODO: allow new setting name to get added here and added to local.settings.json + if (project) { + const settings = await azureFunctionsUtils.getLocalSettingsJson(path.join(path.dirname(project!), constants.azureFunctionLocalSettingsFileName)); + const existingSettings: string[] = settings.Values ? Object.keys(settings.Values) : []; + + connectionStringSettingName = await vscode.window.showQuickPick(existingSettings, { + canPickMany: false, + title: constants.selectSetting, + ignoreFocusOut: true + }); + } 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, connectionStringSetting); + const result = await azureFunctionsService.addSqlBinding(selectedBinding.type, uri.fsPath, azureFunctionName, objectName, connectionStringSettingName); if (!result.success) { void vscode.window.showErrorMessage(result.errorMessage); diff --git a/extensions/sql-database-projects/src/tools/packageHelper.ts b/extensions/sql-database-projects/src/tools/packageHelper.ts index 548452edbe..cbae1837f6 100644 --- a/extensions/sql-database-projects/src/tools/packageHelper.ts +++ b/extensions/sql-database-projects/src/tools/packageHelper.ts @@ -2,10 +2,9 @@ * 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 azureFunctionsUtils from '../common/azureFunctionsUtils'; import * as constants from '../common/constants'; import { DotNetCommandOptions, NetCoreTool } from './netcoreTool'; @@ -57,48 +56,19 @@ export class PackageHelper { */ public async addPackageToAFProjectContainingFile(filePath: string, packageName: string, packageVersion?: string): Promise { try { - const project = await this.getAFProjectContainingFile(filePath); + const project = await azureFunctionsUtils.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); + } else { + void vscode.window.showInformationMessage(`To use SQL bindings, ensure your Azure Functions project has a reference to ${constants.sqlExtensionPackageName}`); } } catch (e) { void 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 - void 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)); - } } diff --git a/extensions/sql-database-projects/yarn.lock b/extensions/sql-database-projects/yarn.lock index b1220e4d08..b16190eb3c 100644 --- a/extensions/sql-database-projects/yarn.lock +++ b/extensions/sql-database-projects/yarn.lock @@ -799,6 +799,11 @@ json5@^2.1.2: dependencies: minimist "^1.2.5" +jsonc-parser@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.3.1.tgz#59549150b133f2efacca48fe9ce1ec0659af2342" + integrity sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg== + jsonfile@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"