Load settings from local.settings.json for add sql bindings quickpick (#16957)

* load settings from local.settings.json for add sql bindings quickpick

* cleanup

* addressing comments
This commit is contained in:
Kim Santiago
2021-09-02 17:25:13 -07:00
committed by GitHub
parent 0bc2a50d78
commit a47276f708
7 changed files with 167 additions and 46 deletions

View File

@@ -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",

View File

@@ -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<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
};
}
/**
* 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<string | 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 ' + 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<boolean> {
return fse.pathExists(path.join(folderPath, constants.hostFileName));
}

View File

@@ -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); }

View File

@@ -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<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

@@ -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<void> {
@@ -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);

View File

@@ -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<void> {
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<string | 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 => 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<boolean> {
return await fse.pathExists(path.join(folderPath, constants.hostFileName));
}
}

View File

@@ -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"