diff --git a/extensions/sql-database-projects/src/common/apiWrapper.ts b/extensions/sql-database-projects/src/common/apiWrapper.ts index c7cff38f9b..a7c3ae385c 100644 --- a/extensions/sql-database-projects/src/common/apiWrapper.ts +++ b/extensions/sql-database-projects/src/common/apiWrapper.ts @@ -11,26 +11,98 @@ import * as azdata from 'azdata'; * this API from our code */ export class ApiWrapper { - public createOutputChannel(name: string): vscode.OutputChannel { - return vscode.window.createOutputChannel(name); + //#region azdata.accounts + + public getAllAccounts(): Thenable { + return azdata.accounts.getAllAccounts(); } - public createTerminalWithOptions(options: vscode.TerminalOptions): vscode.Terminal { - return vscode.window.createTerminal(options); + public getSecurityToken(account: azdata.Account, resource: azdata.AzureResource): Thenable<{ [key: string]: any }> { + return azdata.accounts.getSecurityToken(account, resource); } + //#endregion + + //#region azdata.connection + public getCurrentConnection(): Thenable { return azdata.connection.getCurrentConnection(); } - public openConnectionDialog(): Thenable { - return azdata.connection.openConnectionDialog(); + public openConnectionDialog(providers?: string[], + initialConnectionProfile?: azdata.IConnectionProfile, + connectionCompletionOptions?: azdata.IConnectionCompletionOptions): Thenable { + return azdata.connection.openConnectionDialog(providers, initialConnectionProfile, connectionCompletionOptions); } public getCredentials(connectionId: string): Thenable<{ [name: string]: string }> { return azdata.connection.getCredentials(connectionId); } + public connectionConnect(connectionProfile: azdata.IConnectionProfile, saveConnection?: boolean, showDashboard?: boolean): Thenable { + return azdata.connection.connect(connectionProfile, saveConnection, showDashboard); + } + + public getUriForConnection(connectionId: string): Thenable { + return azdata.connection.getUriForConnection(connectionId); + } + + public listDatabases(connectionId: string): Thenable { + return azdata.connection.listDatabases(connectionId); + } + + //#endregion + + //#region azdata.dataprotocol + + public getProvider(providerId: string, providerType: azdata.DataProviderType): T { + return azdata.dataprotocol.getProvider(providerId, providerType); + } + + //#endregion + + //#region azdata.queryeditor + + public connect(fileUri: string, connectionId: string): Thenable { + return azdata.queryeditor.connect(fileUri, connectionId); + } + + public runQuery(fileUri: string, options?: Map, runCurrentQuery?: boolean): void { + azdata.queryeditor.runQuery(fileUri, options, runCurrentQuery); + } + + //#endregion + + //#region azdata.tasks + + public registerTaskHandler(taskId: string, handler: (profile: azdata.IConnectionProfile) => void): void { + azdata.tasks.registerTask(taskId, handler); + } + + public startBackgroundOperation(operationInfo: azdata.BackgroundOperationInfo): void { + azdata.tasks.startBackgroundOperation(operationInfo); + } + + //#endregion + + //#region azdata.ui + + public registerWidget(widgetId: string, handler: (view: azdata.ModelView) => void): void { + azdata.ui.registerModelViewProvider(widgetId, handler); + } + + //#endregion + + //#region azdata.window + + public closeDialog(dialog: azdata.window.Dialog) { + azdata.window.closeDialog(dialog); + } + + //#endregion + + //#region vscode.commands + public registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): vscode.Disposable { return vscode.commands.registerCommand(command, callback, thisArg); } @@ -39,26 +111,38 @@ export class ApiWrapper { return vscode.commands.executeCommand(command, ...rest); } - public registerTaskHandler(taskId: string, handler: (profile: azdata.IConnectionProfile) => void): void { - azdata.tasks.registerTask(taskId, handler); + //#endregion + + //#region vscode.env + + public openExternal(target: vscode.Uri): Thenable { + return vscode.env.openExternal(target); + } + + //#endregion + + //#region vscode.extensions + + public getExtension(extensionId: string): vscode.Extension | undefined { + return vscode.extensions.getExtension(extensionId); + } + + //#endregion + + //#region vscode.window + + public createOutputChannel(name: string): vscode.OutputChannel { + return vscode.window.createOutputChannel(name); + } + + public createTerminalWithOptions(options: vscode.TerminalOptions): vscode.Terminal { + return vscode.window.createTerminal(options); } public registerTreeDataProvider(viewId: string, treeDataProvider: vscode.TreeDataProvider): vscode.Disposable { return vscode.window.registerTreeDataProvider(viewId, treeDataProvider); } - public createTreeView(viewId: string, options: vscode.TreeViewOptions): vscode.TreeView { - return vscode.window.createTreeView(viewId, options); - } - - public getUriForConnection(connectionId: string): Thenable { - return azdata.connection.getUriForConnection(connectionId); - } - - public getProvider(providerId: string, providerType: azdata.DataProviderType): T { - return azdata.dataprotocol.getProvider(providerId, providerType); - } - public showErrorMessage(message: string, ...items: string[]): Thenable { return vscode.window.showErrorMessage(message, ...items); } @@ -75,26 +159,6 @@ export class ApiWrapper { return vscode.window.showOpenDialog(options); } - public startBackgroundOperation(operationInfo: azdata.BackgroundOperationInfo): void { - azdata.tasks.startBackgroundOperation(operationInfo); - } - - public openExternal(target: vscode.Uri): Thenable { - return vscode.env.openExternal(target); - } - - public getExtension(extensionId: string): vscode.Extension | undefined { - return vscode.extensions.getExtension(extensionId); - } - - public getConfiguration(section?: string, resource?: vscode.Uri | null): vscode.WorkspaceConfiguration { - return vscode.workspace.getConfiguration(section, resource); - } - - public workspaceFolders(): readonly vscode.WorkspaceFolder[] | undefined { - return vscode.workspace.workspaceFolders; - } - public createTab(title: string): azdata.window.DialogTab { return azdata.window.createTab(title); } @@ -115,14 +179,6 @@ export class ApiWrapper { return azdata.window.openDialog(dialog); } - public getAllAccounts(): Thenable { - return azdata.accounts.getAllAccounts(); - } - - public getSecurityToken(account: azdata.Account, resource: azdata.AzureResource): Thenable<{ [key: string]: any }> { - return azdata.accounts.getSecurityToken(account, resource); - } - public showQuickPick(items: T[] | Thenable, options?: vscode.QuickPickOptions, token?: vscode.CancellationToken): Thenable { return vscode.window.showQuickPick(items, options, token); } @@ -135,22 +191,6 @@ export class ApiWrapper { return vscode.window.showSaveDialog(options); } - public listDatabases(connectionId: string): Thenable { - return azdata.connection.listDatabases(connectionId); - } - - public openTextDocument(options?: { language?: string; content?: string; }): Thenable { - return vscode.workspace.openTextDocument(options); - } - - public connect(fileUri: string, connectionId: string): Thenable { - return azdata.queryeditor.connect(fileUri, connectionId); - } - - public runQuery(fileUri: string, options?: Map, runCurrentQuery?: boolean): void { - azdata.queryeditor.runQuery(fileUri, options, runCurrentQuery); - } - public showTextDocument(uri: vscode.Uri, options?: vscode.TextDocumentShowOptions): Thenable { return vscode.window.showTextDocument(uri, options); } @@ -159,7 +199,25 @@ export class ApiWrapper { return azdata.window.createButton(label, position); } - public registerWidget(widgetId: string, handler: (view: azdata.ModelView) => void): void { - azdata.ui.registerModelViewProvider(widgetId, handler); + public createTreeView(viewId: string, options: vscode.TreeViewOptions): vscode.TreeView { + return vscode.window.createTreeView(viewId, options); } + + //#endregion + + //#region vscode.workspace + + public getConfiguration(section?: string, resource?: vscode.Uri | null): vscode.WorkspaceConfiguration { + return vscode.workspace.getConfiguration(section, resource); + } + + public workspaceFolders(): readonly vscode.WorkspaceFolder[] | undefined { + return vscode.workspace.workspaceFolders; + } + + public openTextDocument(options?: { language?: string; content?: string; }): Thenable { + return vscode.workspace.openTextDocument(options); + } + + //#endregion } diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index 906c079037..9b6d1c3343 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -120,6 +120,8 @@ export const Files = 'Files'; export const PackageReference = 'PackageReference'; export const Version = 'Version'; export const PrivateAssets = 'PrivateAssets'; +export const SqlCmdVariable = 'SqlCmdVariable'; +export const DefaultValue = 'DefaultValue'; export const ArtifactReference = 'ArtifactReference'; export const SuppressMissingDependenciesErrors = 'SuppressMissingDependenciesErrors'; export const DatabaseVariableLiteralValue = 'DatabaseVariableLiteralValue'; diff --git a/extensions/sql-database-projects/src/dialogs/deployDatabaseDialog.ts b/extensions/sql-database-projects/src/dialogs/deployDatabaseDialog.ts index 370caa4376..fe2a991fbc 100644 --- a/extensions/sql-database-projects/src/dialogs/deployDatabaseDialog.ts +++ b/extensions/sql-database-projects/src/dialogs/deployDatabaseDialog.ts @@ -136,10 +136,10 @@ export class DeployDatabaseDialog { }; if (dataSource.integratedSecurity) { - connId = (await azdata.connection.connect(connProfile, false, false)).connectionId; + connId = (await this.apiWrapper.connectionConnect(connProfile, false, false)).connectionId; } else { - connId = (await azdata.connection.openConnectionDialog(undefined, connProfile)).connectionId; + connId = (await this.apiWrapper.openConnectionDialog(undefined, connProfile)).connectionId; } } else { @@ -150,7 +150,7 @@ export class DeployDatabaseDialog { connId = this.connection?.connectionId; } - return await azdata.connection.getUriForConnection(connId); + return await this.apiWrapper.getUriForConnection(connId); } public async deployClick(): Promise { @@ -161,7 +161,7 @@ export class DeployDatabaseDialog { sqlCmdVariables: this.project.sqlCmdVariables }; - azdata.window.closeDialog(this.dialog); + this.apiWrapper.closeDialog(this.dialog); await this.deploy!(this.project, profile); this.dispose(); @@ -170,10 +170,11 @@ export class DeployDatabaseDialog { public async generateScriptClick(): Promise { const profile: IGenerateScriptProfile = { databaseName: this.getTargetDatabaseName(), - connectionUri: await this.getConnectionUri() + connectionUri: await this.getConnectionUri(), + sqlCmdVariables: this.project.sqlCmdVariables }; - azdata.window.closeDialog(this.dialog); + this.apiWrapper.closeDialog(this.dialog); if (this.generateScript) { await this.generateScript!(this.project, profile); @@ -182,7 +183,7 @@ export class DeployDatabaseDialog { this.dispose(); } - private getTargetDatabaseName(): string { + public getTargetDatabaseName(): string { return this.targetDatabaseTextBox?.value ?? ''; } diff --git a/extensions/sql-database-projects/src/models/project.ts b/extensions/sql-database-projects/src/models/project.ts index 5e911e3c3c..e74ba36daf 100644 --- a/extensions/sql-database-projects/src/models/project.ts +++ b/extensions/sql-database-projects/src/models/project.ts @@ -63,6 +63,15 @@ export class Project { this.importedTargets.push(importTarget.getAttribute(constants.Project)); } + // find all SQLCMD variables to include + for (let i = 0; i < this.projFileXmlDoc.documentElement.getElementsByTagName(constants.SqlCmdVariable).length; i++) { + const sqlCmdVar = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.SqlCmdVariable)[i]; + const varName = sqlCmdVar.getAttribute(constants.Include); + + const varValue = sqlCmdVar.getElementsByTagName(constants.DefaultValue)[0].childNodes[0].nodeValue; + this.sqlCmdVariables[varName] = varValue; + } + // find all database references to include for (let r = 0; r < this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ArtifactReference).length; r++) { const filepath = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ArtifactReference)[r].getAttribute(constants.Include); diff --git a/extensions/sql-database-projects/src/test/baselines/openSqlProjectBaseline.xml b/extensions/sql-database-projects/src/test/baselines/openSqlProjectBaseline.xml index a2c072f2d5..19daaba5c3 100644 --- a/extensions/sql-database-projects/src/test/baselines/openSqlProjectBaseline.xml +++ b/extensions/sql-database-projects/src/test/baselines/openSqlProjectBaseline.xml @@ -73,6 +73,16 @@ + + + MyProdDatabase + $(SqlCmdVar__1) + + + MyBackupDatabase + $(SqlCmdVar__2) + + False diff --git a/extensions/sql-database-projects/src/test/deployDatabaseDialog.test.ts b/extensions/sql-database-projects/src/test/deployDatabaseDialog.test.ts index 685ac63506..a0231444e8 100644 --- a/extensions/sql-database-projects/src/test/deployDatabaseDialog.test.ts +++ b/extensions/sql-database-projects/src/test/deployDatabaseDialog.test.ts @@ -9,22 +9,29 @@ import * as os from 'os'; import * as vscode from 'vscode'; import * as baselines from './baselines/baselines'; import * as templates from '../templates/templates'; +import * as testUtils from '../test/testUtils'; +import * as TypeMoq from 'typemoq'; + import { DeployDatabaseDialog } from '../dialogs/deployDatabaseDialog'; import { Project } from '../models/project'; import { SqlDatabaseProjectTreeViewProvider } from '../controllers/databaseProjectTreeViewProvider'; import { ProjectsController } from '../controllers/projectController'; import { createContext, TestContext } from './testContext'; +import { IDeploymentProfile, IGenerateScriptProfile } from '../models/IDeploymentProfile'; let testContext: TestContext; describe('Deploy Database Dialog', () => { before(async function (): Promise { - testContext = createContext(); await templates.loadTemplates(path.join(__dirname, '..', '..', 'resources', 'templates')); await baselines.loadBaselines(); }); + beforeEach(async function (): Promise { + testContext = createContext(); + }); + it('Should open dialog successfully ', async function (): Promise { const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider()); const projFileDir = path.join(os.tmpdir(), `TestProject_${new Date().getTime()}`); @@ -47,4 +54,43 @@ describe('Deploy Database Dialog', () => { const deployDatabaseDialog = new DeployDatabaseDialog(testContext.apiWrapper.object, project); should.equal(deployDatabaseDialog.getDefaultDatabaseName(), project.projectFileName); }); + + it('Should include all info in deployment profile', async function (): Promise { + const proj = await testUtils.createTestProject(baselines.openProjectFileBaseline); + const dialog = TypeMoq.Mock.ofType(DeployDatabaseDialog, undefined, undefined, testContext.apiWrapper.object, proj); + dialog.setup(x => x.getConnectionUri()).returns(async () => { return 'Mock|Connection|Uri'; }); + dialog.setup(x => x.getTargetDatabaseName()).returns(() => 'MockDatabaseName'); + dialog.callBase = true; + + let profile: IDeploymentProfile | IGenerateScriptProfile | undefined; + + const expectedDeploy: IDeploymentProfile = { + databaseName: 'MockDatabaseName', + connectionUri: 'Mock|Connection|Uri', + upgradeExisting: true, + sqlCmdVariables: { + 'ProdDatabaseName': 'MyProdDatabase', + 'BackupDatabaseName': 'MyBackupDatabase' + } + }; + + dialog.object.deploy = async (_, prof) => { profile = prof; }; + await dialog.object.deployClick(); + + should(profile).deepEqual(expectedDeploy); + + const expectedGenScript: IGenerateScriptProfile = { + databaseName: 'MockDatabaseName', + connectionUri: 'Mock|Connection|Uri', + sqlCmdVariables: { + 'ProdDatabaseName': 'MyProdDatabase', + 'BackupDatabaseName': 'MyBackupDatabase' + } + }; + + dialog.object.generateScript = async (_, prof) => { profile = prof; }; + await dialog.object.generateScriptClick(); + + should(profile).deepEqual(expectedGenScript); + }); }); diff --git a/extensions/sql-database-projects/src/test/project.test.ts b/extensions/sql-database-projects/src/test/project.test.ts index bff21efe4a..51b3ed7e16 100644 --- a/extensions/sql-database-projects/src/test/project.test.ts +++ b/extensions/sql-database-projects/src/test/project.test.ts @@ -31,12 +31,19 @@ describe('Project: sqlproj content operations', function (): void { const project: Project = new Project(projFilePath); await project.readProjFile(); + // Files and folders should(project.files.filter(f => f.type === EntryType.File).length).equal(4); should(project.files.filter(f => f.type === EntryType.Folder).length).equal(5); should(project.files.find(f => f.type === EntryType.Folder && f.relativePath === 'Views\\User')).not.equal(undefined); // mixed ItemGroup folder should(project.files.find(f => f.type === EntryType.File && f.relativePath === 'Views\\User\\Profile.sql')).not.equal(undefined); // mixed ItemGroup file + // SqlCmdVariables + should(Object.keys(project.sqlCmdVariables).length).equal(2); + should(project.sqlCmdVariables['ProdDatabaseName']).equal('MyProdDatabase'); + should(project.sqlCmdVariables['BackupDatabaseName']).equal('MyBackupDatabase'); + + // Database references should(project.databaseReferences.length).equal(1); should(project.databaseReferences[0]).containEql(constants.master); }); diff --git a/extensions/sql-database-projects/src/test/testUtils.ts b/extensions/sql-database-projects/src/test/testUtils.ts index a3abbc5aeb..4715c2d555 100644 --- a/extensions/sql-database-projects/src/test/testUtils.ts +++ b/extensions/sql-database-projects/src/test/testUtils.ts @@ -32,7 +32,10 @@ export async function createTestSqlProjFile(contents: string, folderPath?: strin } export async function createTestProject(contents: string, folderPath?: string): Promise { - return new Project(await createTestSqlProjFile(contents, folderPath)); + const proj = new Project(await createTestSqlProjFile(contents, folderPath)); + await proj.readProjFile(); + + return proj; } export async function createTestDataSources(contents: string, folderPath?: string): Promise { diff --git a/extensions/sql-database-projects/src/test/utils.test.ts b/extensions/sql-database-projects/src/test/utils.test.ts index de28347e6c..b6318cd05a 100644 --- a/extensions/sql-database-projects/src/test/utils.test.ts +++ b/extensions/sql-database-projects/src/test/utils.test.ts @@ -17,7 +17,7 @@ describe('Tests for conversion within PascalCase and camelCase', function (): vo }); describe('Tests to verify exists function', function (): void { - it('Should determine existance of files/folders', async () => { + it('Should determine existence of files/folders', async () => { let testFolderPath = await createDummyFileStructure(); should(await exists(testFolderPath)).equal(true);