From 91ea2b43d6cf8cd1b17570cce9cc0012ece53498 Mon Sep 17 00:00:00 2001 From: Sakshi Sharma <57200045+SakshiS-harma@users.noreply.github.com> Date: Thu, 13 Apr 2023 17:08:24 -0700 Subject: [PATCH] Save publish profile in Publish UI workflow (#22700) * Add profile section in Publish project UI * Move publish profile row below Publish Target * Add contract for savePublishProfie and SaveProfileAs button functionality * Make the DacFx contract functional * Send values from UI to DacFx service call * Fix build error * Address comment, remove print statements * Address comments * Set correct connection string * Fix functionality for rename, exclude, delete publish profiles. Add new profile to the tree and sqlproj. * Address comment to update alignement of button * Address comments * Update button to use title casing --- .../dacpac/src/test/testDacFxService.ts | 4 + extensions/mssql/src/contracts.ts | 12 +++ extensions/mssql/src/dacfx/dacFxService.ts | 5 + extensions/mssql/src/mssql.d.ts | 1 + .../src/common/constants.ts | 2 + .../src/controllers/projectController.ts | 17 ++- .../src/dialogs/publishDatabaseDialog.ts | 100 +++++++++++++----- .../src/models/project.ts | 1 + .../models/publishProfile/publishProfile.ts | 8 ++ .../src/test/testContext.ts | 26 +++-- extensions/types/vscode-mssql.d.ts | 9 ++ 11 files changed, 148 insertions(+), 37 deletions(-) diff --git a/extensions/dacpac/src/test/testDacFxService.ts b/extensions/dacpac/src/test/testDacFxService.ts index e10d604854..b08a727557 100644 --- a/extensions/dacpac/src/test/testDacFxService.ts +++ b/extensions/dacpac/src/test/testDacFxService.ts @@ -89,4 +89,8 @@ export class DacFxTestService implements mssql.IDacFxService { parseTSqlScript(filePath: string, databaseSchemaProvider: string): Thenable { return Promise.resolve({ containsCreateTableStatement: true }); } + + savePublishProfile(profilePath: string, databaseName: string, connectionString: string, sqlCommandVariableValues?: Record): Thenable { + return Promise.resolve(this.dacfxResult); + } } diff --git a/extensions/mssql/src/contracts.ts b/extensions/mssql/src/contracts.ts index b32cac9356..a0d81987bf 100644 --- a/extensions/mssql/src/contracts.ts +++ b/extensions/mssql/src/contracts.ts @@ -551,6 +551,14 @@ export interface ParseTSqlScriptParams { databaseSchemaProvider: string; } +export interface SavePublishProfileParams { + profilePath: string; + databaseName: string; + connectionString: string; + sqlCommandVariableValues?: Record; + deploymentOptions?: mssql.DeploymentOptions; +} + export namespace ExportRequest { export const type = new RequestType('dacfx/export'); } @@ -587,6 +595,10 @@ export namespace ParseTSqlScriptRequest { export const type = new RequestType('dacfx/parseTSqlScript'); } +export namespace SavePublishProfileRequest { + export const type = new RequestType('dacfx/savePublishProfile'); +} + // ------------------------------- ------------------------------------ // ------------------------------- < Sql Projects > ------------------------------------ diff --git a/extensions/mssql/src/dacfx/dacFxService.ts b/extensions/mssql/src/dacfx/dacFxService.ts index 21d9c7cbae..9c4dc4d734 100644 --- a/extensions/mssql/src/dacfx/dacFxService.ts +++ b/extensions/mssql/src/dacfx/dacFxService.ts @@ -84,4 +84,9 @@ export class DacFxService extends BaseService implements mssql.IDacFxService { const params: contracts.ParseTSqlScriptParams = { filePath, databaseSchemaProvider }; return this.runWithErrorHandling(contracts.ParseTSqlScriptRequest.type, params); } + + public async savePublishProfile(profilePath: string, databaseName: string, connectionString: string, sqlCommandVariableValues?: Record, deploymentOptions?: mssql.DeploymentOptions): Promise { + const params: contracts.SavePublishProfileParams = { profilePath, databaseName, connectionString, sqlCommandVariableValues, deploymentOptions }; + return this.runWithErrorHandling(contracts.SavePublishProfileRequest.type, params); + } } diff --git a/extensions/mssql/src/mssql.d.ts b/extensions/mssql/src/mssql.d.ts index 57558b15f9..c35f12616b 100644 --- a/extensions/mssql/src/mssql.d.ts +++ b/extensions/mssql/src/mssql.d.ts @@ -242,6 +242,7 @@ declare module 'mssql' { getOptionsFromProfile(profilePath: string): Thenable; validateStreamingJob(packageFilePath: string, createStreamingJobTsql: string): Thenable; parseTSqlScript(filePath: string, databaseSchemaProvider: string): Thenable; + savePublishProfile(profilePath: string, databaseName: string, connectionString: string, sqlCommandVariableValues?: Record, deploymentOptions?: DeploymentOptions): Thenable; } export interface DacFxResult extends azdata.ResultStatus { diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index 9949c4613a..145960f117 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -149,6 +149,8 @@ export const server = localize('server', "Server"); export const defaultUser = localize('default', "default"); export const selectProfileToUse = localize('selectProfileToUse', "Select publish profile to load"); export const selectProfile = localize('selectProfile', "Select Profile"); +export const saveProfileAsButtonText = localize('saveProfileAsButtonText', "Save Profile as..."); +export const save = localize('save', "Save"); export const dontUseProfile = localize('dontUseProfile', "Don't use profile"); export const browseForProfileWithIcon = `$(folder) ${localize('browseForProfile', "Browse for profile")}`; export const chooseAction = localize('chooseAction', "Choose action"); diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index 6d0a24e0cc..7a8fa90bed 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -25,7 +25,7 @@ import { ImportDataModel } from '../models/api/import'; import { NetCoreTool, DotNetError } from '../tools/netcoreTool'; import { ShellCommandOptions } from '../tools/shellExecutionHelper'; import { BuildHelper } from '../tools/buildHelper'; -import { readPublishProfile } from '../models/publishProfile/publishProfile'; +import { readPublishProfile, savePublishProfile } from '../models/publishProfile/publishProfile'; import { AddDatabaseReferenceDialog } from '../dialogs/addDatabaseReferenceDialog'; import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings, IProjectReferenceSettings } from '../models/IDatabaseReferenceSettings'; import { DatabaseReferenceTreeItem } from '../models/tree/databaseReferencesTreeItem'; @@ -445,6 +445,7 @@ export class ProjectsController { publishDatabaseDialog.publishToContainer = async (proj, prof) => this.publishToDockerContainer(proj, prof); publishDatabaseDialog.generateScript = async (proj, prof) => this.publishOrScriptProject(proj, prof, false); publishDatabaseDialog.readPublishProfile = async (profileUri) => readPublishProfile(profileUri); + publishDatabaseDialog.savePublishProfile = async (profilePath, databaseName, connectionString, sqlCommandVariableValues, deploymentOptions) => savePublishProfile(profilePath, databaseName, connectionString, sqlCommandVariableValues, deploymentOptions); publishDatabaseDialog.openDialog(); @@ -820,6 +821,7 @@ export class ProjectsController { await project.excludePostDeploymentScript(fileEntry.relativePath); break; case constants.DatabaseProjectItemType.noneFile: + case constants.DatabaseProjectItemType.publishProfile: await project.excludeNoneItem(fileEntry.relativePath); break; default: @@ -879,6 +881,7 @@ export class ProjectsController { await project.deletePostDeploymentScript(node.entryKey); break; case constants.DatabaseProjectItemType.noneFile: + case constants.DatabaseProjectItemType.publishProfile: await project.deleteNoneItem(node.entryKey); break; default: @@ -903,13 +906,21 @@ export class ProjectsController { const node = context.element as BaseProjectTreeItem; const project = await this.getProjectFromContext(node); const file = this.getFileProjectEntry(project, node); + const baseName = path.basename(node.friendlyName); + let fileExtension: string; + + if (utils.isPublishProfile(baseName)) { + fileExtension = constants.publishProfileExtension; + } else { + fileExtension = constants.sqlFileExtension; + } // need to use quickpick because input box isn't supported in treeviews // https://github.com/microsoft/vscode/issues/117502 and https://github.com/microsoft/vscode/issues/97190 const newFileName = await vscode.window.showInputBox( { title: constants.enterNewName, - value: path.basename(node.friendlyName, constants.sqlFileExtension), + value: path.basename(baseName, fileExtension), ignoreFocusOut: true, validateInput: async (value) => { return await this.fileAlreadyExists(value, file?.fsUri.fsPath!) ? constants.fileAlreadyExists(value) : undefined; @@ -920,7 +931,7 @@ export class ProjectsController { return; } - const newFilePath = path.join(path.dirname(utils.getPlatformSafeFileEntryPath(file?.relativePath!)), `${newFileName}.sql`); + const newFilePath = path.join(path.dirname(utils.getPlatformSafeFileEntryPath(file?.relativePath!)), `${newFileName}${fileExtension}`); const renameResult = await project.move(node, newFilePath); diff --git a/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts b/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts index 0f85f94925..9d4f04ffd2 100644 --- a/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts +++ b/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts @@ -8,6 +8,7 @@ import * as vscode from 'vscode'; import * as constants from '../common/constants'; import * as utils from '../common/utils'; import * as uiUtils from './utils'; +import * as path from 'path'; import { Project } from '../models/project'; import { SqlConnectionDataSource } from '../models/dataSources/sqlConnectionStringSource'; @@ -62,6 +63,7 @@ export class PublishDatabaseDialog { protected optionsButton: azdataType.ButtonComponent | undefined; private publishOptionsDialog: PublishOptionsDialog | undefined; public publishOptionsModified: boolean = false; + private publishProfileUri: vscode.Uri | undefined; private completionPromise: Deferred = new Deferred(); @@ -71,6 +73,7 @@ export class PublishDatabaseDialog { public publishToContainer: ((proj: Project, profile: IPublishToDockerSettings) => any) | undefined; public generateScript: ((proj: Project, profile: ISqlProjectPublishSettings) => any) | undefined; public readPublishProfile: ((profileUri: vscode.Uri) => any) | undefined; + public savePublishProfile: ((profilePath: string, databaseName: string, connectionString: string, sqlCommandVariableValues?: Record, deploymentOptions?: DeploymentOptions) => any) | undefined; constructor(private project: Project) { this.dialog = utils.getAzdataApi()!.window.createModelViewDialog(constants.publishDialogName, 'sqlProjectPublishDialog'); @@ -144,13 +147,14 @@ export class PublishDatabaseDialog { const options = await this.getDefaultDeploymentOptions(); this.setDeploymentOptions(options); - const profileRow = this.createProfileRow(view); + const profileRow = this.createProfileSection(view); + this.connectionRow = this.createConnectionRow(view); this.databaseRow = this.createDatabaseRow(view); const displayOptionsButton = this.createOptionsButton(view); const horizontalFormSection = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); - horizontalFormSection.addItems([profileRow, this.databaseRow]); + horizontalFormSection.addItems([this.databaseRow]); this.formBuilder = view.modelBuilder.formContainer() .withFormItems([ @@ -161,6 +165,10 @@ export class PublishDatabaseDialog { component: flexRadioButtonsModel, title: '' }, + { + component: profileRow, + title: constants.profile + }, { component: this.connectionRow, title: '' @@ -432,18 +440,20 @@ export class PublishDatabaseDialog { this.createDatabaseRow(view); this.tryEnableGenerateScriptAndPublishButtons(); if (existingServer) { - if (this.connectionRow) { - this.formBuilder!.insertFormItem({ - title: '', - component: this.connectionRow - }, 2); - } if (this.localDbSection) { this.formBuilder!.removeFormItem({ title: '', component: this.localDbSection }); } + + if (this.connectionRow) { + this.formBuilder!.insertFormItem({ + title: '', + component: this.connectionRow + }, 3); + } + } else { if (this.connectionRow) { this.formBuilder!.removeFormItem({ @@ -451,6 +461,7 @@ export class PublishDatabaseDialog { component: this.connectionRow }); } + if (this.localDbSection) { this.formBuilder!.insertFormItem({ title: '', @@ -524,22 +535,19 @@ export class PublishDatabaseDialog { } } - private createProfileRow(view: azdataType.ModelView): azdataType.FlexContainer { - const loadProfileButton = this.createLoadProfileButton(view); + private createProfileSection(view: azdataType.ModelView): azdataType.FlexContainer { + const selectProfileButton = this.createSelectProfileButton(view); + const saveProfileAsButton = this.createSaveProfileAsButton(view); + this.loadProfileTextBox = view.modelBuilder.inputBox().withProps({ placeHolder: constants.loadProfilePlaceholderText, ariaLabel: constants.profile, - width: cssStyles.publishDialogTextboxWidth, + width: '200px', enabled: false }).component(); - const profileLabel = view.modelBuilder.text().withProps({ - value: constants.profile, - width: cssStyles.publishDialogLabelWidth - }).component(); - - const profileRow = view.modelBuilder.flexContainer().withItems([profileLabel, this.loadProfileTextBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); - profileRow.insertItem(loadProfileButton, 2, { CSSStyles: { 'margin-right': '0px' } }); + const buttonsList = view.modelBuilder.flexContainer().withItems([selectProfileButton, saveProfileAsButton], { flex: '0 0 auto', CSSStyles: { 'margin-right': '5px', 'text-align': 'justify' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); + const profileRow = view.modelBuilder.flexContainer().withItems([this.loadProfileTextBox, buttonsList], { flex: '0 0 auto', CSSStyles: { 'margin-right': '15px', 'text-align': 'justify' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); return profileRow; } @@ -862,13 +870,14 @@ export class PublishDatabaseDialog { } } - private createLoadProfileButton(view: azdataType.ModelView): azdataType.ButtonComponent { + private createSelectProfileButton(view: azdataType.ModelView): azdataType.ButtonComponent { let loadProfileButton: azdataType.ButtonComponent = view.modelBuilder.button().withProps({ - ariaLabel: constants.loadProfilePlaceholderText, - title: constants.loadProfilePlaceholderText, - iconPath: IconPathHelper.folder_blue, - height: '18px', - width: '18px' + label: constants.selectProfile, + title: constants.selectProfile, + ariaLabel: constants.selectProfile, + width: '90px', + height: '25px', + secondary: true, }).component(); loadProfileButton.onDidClick(async () => { @@ -917,12 +926,55 @@ export class PublishDatabaseDialog { await this.loadProfileTextBox!.updateProperty('title', fileUris[0].fsPath); this.profileUsed = true; + this.publishProfileUri = fileUris[0]; } }); return loadProfileButton; } + private createSaveProfileAsButton(view: azdataType.ModelView): azdataType.ButtonComponent { + let saveProfileAsButton: azdataType.ButtonComponent = view.modelBuilder.button().withProps({ + label: constants.saveProfileAsButtonText, + title: constants.saveProfileAsButtonText, + ariaLabel: constants.saveProfileAsButtonText, + width: cssStyles.PublishingOptionsButtonWidth, + height: '25px', + secondary: true + }).component(); + + saveProfileAsButton.onDidClick(async () => { + const filePath = await vscode.window.showSaveDialog( + { + defaultUri: this.publishProfileUri ?? vscode.Uri.file(path.join(this.project.projectFolderPath, `${this.project.projectFileName}_1`)), + saveLabel: constants.save, + filters: { + 'Publish Settings Files': ['publish.xml'], + } + } + ); + + if (!filePath) { + return; + } + + if (this.savePublishProfile) { + const targetConnectionString = this.connectionId ? await utils.getAzdataApi()!.connection.getConnectionString(this.connectionId, false) : ''; + const targetDatabaseName = this.targetDatabaseName ?? ''; + const deploymentOptions = await this.getDeploymentOptions(); + await this.savePublishProfile(filePath.fsPath, targetDatabaseName, targetConnectionString, this.getSqlCmdVariablesForPublish(), deploymentOptions); + } + + this.profileUsed = true; + this.publishProfileUri = filePath; + + await this.project.addNoneItem(path.relative(this.project.projectFolderPath, filePath.fsPath)); + void vscode.commands.executeCommand(constants.refreshDataWorkspaceCommand); //refresh data workspace to load the newly added profile to the tree + }); + + return saveProfileAsButton; + } + private convertSqlCmdVarsToTableFormat(sqlCmdVars: Record): azdataType.DeclarativeTableCellValue[][] { let data = []; for (let key in sqlCmdVars) { diff --git a/extensions/sql-database-projects/src/models/project.ts b/extensions/sql-database-projects/src/models/project.ts index f4bdfed334..c1970aad6b 100644 --- a/extensions/sql-database-projects/src/models/project.ts +++ b/extensions/sql-database-projects/src/models/project.ts @@ -441,6 +441,7 @@ export class Project implements ISqlProject { this._noneDeployScripts = []; this._outputPath = ''; this._configuration = Configuration.Debug; + this._publishProfiles = []; } public async updateProjectForCrossPlatform(): Promise { diff --git a/extensions/sql-database-projects/src/models/publishProfile/publishProfile.ts b/extensions/sql-database-projects/src/models/publishProfile/publishProfile.ts index 9b85d6effd..29d53100a8 100644 --- a/extensions/sql-database-projects/src/models/publishProfile/publishProfile.ts +++ b/extensions/sql-database-projects/src/models/publishProfile/publishProfile.ts @@ -125,3 +125,11 @@ async function readConnectionString(xmlDoc: any): Promise<{ connectionId: string server: server }; } + +/** + * saves publish settings to the specified profile file + */ +export async function savePublishProfile(profilePath: string, databaseName: string, connectionString: string, sqlCommandVariableValues?: Record, deploymentOptions?: mssql.DeploymentOptions): Promise { + const dacFxService = await utils.getDacFxService(); + await dacFxService.savePublishProfile(profilePath, databaseName, connectionString, sqlCommandVariableValues, deploymentOptions); +} diff --git a/extensions/sql-database-projects/src/test/testContext.ts b/extensions/sql-database-projects/src/test/testContext.ts index 4fc7d8c8fc..5179470c64 100644 --- a/extensions/sql-database-projects/src/test/testContext.ts +++ b/extensions/sql-database-projects/src/test/testContext.ts @@ -22,6 +22,11 @@ export const mockDacFxResult = { report: '' }; +export const mockSavePublishResult = { + success: true, + errorMessage: '' +}; + /* Get the deployment options sample model */ export function getDeploymentOptions(): mssql.DeploymentOptions { const sampleDesc = 'Sample Description text'; @@ -47,16 +52,17 @@ export const mockDacFxOptionsResult: mssql.DacFxOptionsResult = { }; export class MockDacFxService implements mssql.IDacFxService { - public exportBacpac(_: string, __: string, ___: string, ____: azdata.TaskExecutionMode): Thenable { return Promise.resolve(mockDacFxResult); } - public importBacpac(_: string, __: string, ___: string, ____: azdata.TaskExecutionMode): Thenable { return Promise.resolve(mockDacFxResult); } - public extractDacpac(_: string, __: string, ___: string, ____: string, _____: string, ______: azdata.TaskExecutionMode): Thenable { return Promise.resolve(mockDacFxResult); } - public createProjectFromDatabase(_: string, __: string, ___: string, ____: string, _____: string, ______: mssql.ExtractTarget, _______: azdata.TaskExecutionMode): Thenable { return Promise.resolve(mockDacFxResult); } - public deployDacpac(_: string, __: string, ___: boolean, ____: string, _____: azdata.TaskExecutionMode, ______?: Record): Thenable { return Promise.resolve(mockDacFxResult); } - public generateDeployScript(_: string, __: string, ___: string, ____: azdata.TaskExecutionMode, ______?: Record): Thenable { return Promise.resolve(mockDacFxResult); } - public generateDeployPlan(_: string, __: string, ___: string, ____: azdata.TaskExecutionMode): Thenable { return Promise.resolve(mockDacFxResult); } - public getOptionsFromProfile(_: string): Thenable { return Promise.resolve(mockDacFxOptionsResult); } - public validateStreamingJob(_: string, __: string): Thenable { return Promise.resolve(mockDacFxResult); } - public parseTSqlScript(_: string, __: string): Thenable { return Promise.resolve({ containsCreateTableStatement: true }); } + public exportBacpac(_databaseName: string, _packageFilePath: string, _ownerUri: string, _taskExecutionMode: azdata.TaskExecutionMode): Thenable { return Promise.resolve(mockDacFxResult); } + public importBacpac(_packageFilePath: string, _databaseName: string, _ownerUri: string, _taskExecutionMode: azdata.TaskExecutionMode): Thenable { return Promise.resolve(mockDacFxResult); } + public extractDacpac(_databaseName: string, _packageFilePath: string, _applicationName: string, _applicationVersion: string, _ownerUri: string, _taskExecutionMode: azdata.TaskExecutionMode): Thenable { return Promise.resolve(mockDacFxResult); } + public createProjectFromDatabase(_databaseName: string, _targetFilePath: string, _applicationName: string, _applicationVersion: string, _ownerUri: string, _extractTarget: mssql.ExtractTarget, _taskExecutionMode: azdata.TaskExecutionMode, _includePermissions?: boolean): Thenable { return Promise.resolve(mockDacFxResult); } + public deployDacpac(_packageFilePath: string, _targetDatabaseName: string, _upgradeExisting: boolean, _ownerUri: string, _taskExecutionMode: azdata.TaskExecutionMode, _sqlCommandVariableValues?: Record, _deploymentOptions?: mssql.DeploymentOptions): Thenable { return Promise.resolve(mockDacFxResult); } + public generateDeployScript(_packageFilePath: string, _targetDatabaseName: string, _ownerUri: string, _taskExecutionMode: azdata.TaskExecutionMode, _sqlCommandVariableValues?: Record, _deploymentOptions?: mssql.DeploymentOptions): Thenable { return Promise.resolve(mockDacFxResult); } + public generateDeployPlan(_packageFilePath: string, _targetDatabaseName: string, _ownerUri: string, _taskExecutionMode: azdata.TaskExecutionMode): Thenable { return Promise.resolve(mockDacFxResult); } + public getOptionsFromProfile(_profilePath: string): Thenable { return Promise.resolve(mockDacFxOptionsResult); } + public validateStreamingJob(_packageFilePath: string, _createStreamingJobTsql: string): Thenable { return Promise.resolve(mockDacFxResult); } + public parseTSqlScript(_filePath: string, _databaseSchemaProvider: string): Thenable { return Promise.resolve({ containsCreateTableStatement: true }); } + public savePublishProfile(_profilePath: string, _databaseName: string, _connectionString: string, _sqlCommandVariableValues?: Record, _deploymentOptions?: mssql.DeploymentOptions): Thenable { return Promise.resolve(mockSavePublishResult); } } export function createContext(): TestContext { diff --git a/extensions/types/vscode-mssql.d.ts b/extensions/types/vscode-mssql.d.ts index 3ad7b5b01b..aa63ffe4c5 100644 --- a/extensions/types/vscode-mssql.d.ts +++ b/extensions/types/vscode-mssql.d.ts @@ -431,6 +431,7 @@ declare module 'vscode-mssql' { generateDeployPlan(packageFilePath: string, databaseName: string, ownerUri: string, taskExecutionMode: TaskExecutionMode): Thenable; getOptionsFromProfile(profilePath: string): Thenable; validateStreamingJob(packageFilePath: string, createStreamingJobTsql: string): Thenable; + savePublishProfile(profilePath: string, databaseName: string, connectionString: string, sqlCommandVariableValues?: Record, deploymentOptions?: DeploymentOptions): Thenable; } /** @@ -1106,6 +1107,14 @@ declare module 'vscode-mssql' { defaultDeploymentOptions: DeploymentOptions; } + export interface SavePublishProfileParams { + profilePath: string; + databaseName: string; + connectionString: string; + sqlCommandVariableValues?: Record; + deploymentOptions?: DeploymentOptions; + } + //#region ISqlProjectsService //#region Parameters