diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index 810731e3b4..de873f9394 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -17,7 +17,7 @@ import * as azdata from 'azdata'; import { promises as fs } from 'fs'; import { PublishDatabaseDialog } from '../dialogs/publishDatabaseDialog'; -import { Project, DatabaseReferenceLocation, SystemDatabase, TargetPlatform, ProjectEntry, reservedProjectFolders, SqlProjectReferenceProjectEntry } from '../models/project'; +import { Project, ProjectEntry, reservedProjectFolders, SqlProjectReferenceProjectEntry } from '../models/project'; import { SqlDatabaseProjectTreeViewProvider } from './databaseProjectTreeViewProvider'; import { FolderNode, FileNode } from '../models/tree/fileFolderTreeItem'; import { IPublishSettings, IGenerateScriptSettings } from '../models/IPublishSettings'; @@ -27,6 +27,8 @@ import { ImportDataModel } from '../models/api/import'; import { NetCoreTool, DotNetCommandOptions } from '../tools/netcoreTool'; import { BuildHelper } from '../tools/buildHelper'; import { PublishProfile, load } from '../models/publishProfile/publishProfile'; +import { AddDatabaseReferenceDialog } from '../dialogs/addDatabaseReferenceDialog'; +import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings } from '../models/IDatabaseReferenceSettings'; /** * Controller for managing project lifecycle @@ -429,28 +431,23 @@ export class ProjectsController { * Adds a database reference to the project * @param context a treeItem in a project's hierarchy, to be used to obtain a Project */ - public async addDatabaseReference(context: Project | BaseProjectTreeItem): Promise { + public async addDatabaseReference(context: Project | BaseProjectTreeItem): Promise { const project = this.getProjectFromContext(context); + const addDatabaseReferenceDialog = this.getAddDatabaseReferenceDialog(project); + addDatabaseReferenceDialog.addReference = async (proj, prof) => await this.addDatabaseReferenceCallback(proj, prof); + + addDatabaseReferenceDialog.openDialog(); + + return addDatabaseReferenceDialog; + } + + public async addDatabaseReferenceCallback(project: Project, settings: ISystemDatabaseReferenceSettings | IDacpacReferenceSettings): Promise { try { - // choose if reference is to master or a dacpac - const databaseReferenceType = await this.getDatabaseReferenceType(); - - // if master is selected, we know which dacpac needs to be added - if (databaseReferenceType === constants.systemDatabase) { - const systemDatabase = await this.getSystemDatabaseName(project); - await project.addSystemDatabaseReference(systemDatabase); + if ((settings).systemDb !== undefined) { + await project.addSystemDatabaseReference(settings); } else { - // get other information needed to add a reference to the dacpac - const dacpacFileLocation = await this.getDacpacFileLocation(); - const databaseLocation = await this.getDatabaseLocation(); - - if (databaseLocation === DatabaseReferenceLocation.differentDatabaseSameServer) { - const databaseName = await this.getDatabaseName(dacpacFileLocation); - await project.addDatabaseReference(dacpacFileLocation, databaseLocation, databaseName); - } else { - await project.addDatabaseReference(dacpacFileLocation, databaseLocation); - } + await project.addDatabaseReference(settings); } this.refreshProjectsTree(); @@ -459,120 +456,16 @@ export class ProjectsController { } } - private async getDatabaseReferenceType(): Promise { - let databaseReferenceOptions: vscode.QuickPickItem[] = [ - { - label: constants.systemDatabase - }, - { - label: constants.dacpac - } - ]; - - let input = await vscode.window.showQuickPick(databaseReferenceOptions, { - canPickMany: false, - placeHolder: constants.addDatabaseReferenceInput - }); - - if (!input) { - throw new Error(constants.databaseReferenceTypeRequired); - } - - return input.label; - } - - public async getSystemDatabaseName(project: Project): Promise { - let databaseReferenceOptions: vscode.QuickPickItem[] = [ - { - label: constants.master - } - ]; - - // Azure dbs can only reference master - if (project.getProjectTargetPlatform() !== TargetPlatform.SqlAzureV12) { - databaseReferenceOptions.push( - { - label: constants.msdb - }); - } - - let input = await vscode.window.showQuickPick(databaseReferenceOptions, { - canPickMany: false, - placeHolder: constants.systemDatabaseReferenceInput - }); - - if (!input) { - throw new Error(constants.systemDatabaseReferenceRequired); - } - - return input.label === constants.master ? SystemDatabase.master : SystemDatabase.msdb; - } - - private async getDacpacFileLocation(): Promise { - let fileUris = await vscode.window.showOpenDialog( - { - canSelectFiles: true, - canSelectFolders: false, - canSelectMany: false, - defaultUri: vscode.workspace.workspaceFolders ? (vscode.workspace.workspaceFolders as vscode.WorkspaceFolder[])[0].uri : undefined, - openLabel: constants.selectString, - filters: { - [constants.dacpacFiles]: ['dacpac'], - } - } - ); - - if (!fileUris || fileUris.length === 0) { - throw new Error(constants.dacpacFileLocationRequired); - } - - return fileUris[0]; - } - - private async getDatabaseLocation(): Promise { - let databaseReferenceOptions: vscode.QuickPickItem[] = [ - { - label: constants.databaseReferenceSameDatabase - }, - { - label: constants.databaseReferenceDifferentDabaseSameServer - } - ]; - - let input = await vscode.window.showQuickPick(databaseReferenceOptions, { - canPickMany: false, - placeHolder: constants.databaseReferenceLocation - }); - - if (input === undefined) { - throw new Error(constants.databaseLocationRequired); - } - - const location = input?.label === constants.databaseReferenceSameDatabase ? DatabaseReferenceLocation.sameDatabase : DatabaseReferenceLocation.differentDatabaseSameServer; - return location; - } - - private async getDatabaseName(dacpac: vscode.Uri): Promise { - const dacpacName = path.parse(dacpac.toString()).name; - let databaseName = await vscode.window.showInputBox({ - prompt: constants.databaseReferenceDatabaseName, - value: `${dacpacName}` - }); - - if (!databaseName) { - throw new Error(constants.databaseNameRequired); - } - - databaseName = databaseName?.trim(); - return databaseName; - } - //#region Helper methods public getPublishDialog(project: Project): PublishDatabaseDialog { return new PublishDatabaseDialog(project); } + public getAddDatabaseReferenceDialog(project: Project): AddDatabaseReferenceDialog { + return new AddDatabaseReferenceDialog(project); + } + public async updateProjectForRoundTrip(project: Project) { if (project.importedTargets.includes(constants.NetCoreTargets) && !project.containsSSDTOnlySystemDatabaseReferences()) { return; diff --git a/extensions/sql-database-projects/src/models/IDatabaseReferenceSettings.ts b/extensions/sql-database-projects/src/models/IDatabaseReferenceSettings.ts index b8c412b8bd..3631de0152 100644 --- a/extensions/sql-database-projects/src/models/IDatabaseReferenceSettings.ts +++ b/extensions/sql-database-projects/src/models/IDatabaseReferenceSettings.ts @@ -7,7 +7,7 @@ import { DatabaseReferenceLocation, SystemDatabase } from './project'; import { Uri } from 'vscode'; export interface IDatabaseReferenceSettings { - databaseName: string; + databaseName?: string; } export interface ISystemDatabaseReferenceSettings extends IDatabaseReferenceSettings { diff --git a/extensions/sql-database-projects/src/models/project.ts b/extensions/sql-database-projects/src/models/project.ts index e05f0f2fe4..bf0a3f8db9 100644 --- a/extensions/sql-database-projects/src/models/project.ts +++ b/extensions/sql-database-projects/src/models/project.ts @@ -14,6 +14,7 @@ import * as templates from '../templates/templates'; import { Uri, window } from 'vscode'; import { promises as fs } from 'fs'; import { DataSource } from './dataSources/dataSources'; +import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings } from './IDatabaseReferenceSettings'; /** * Class representing a Project, and providing functions for operating on it @@ -123,7 +124,12 @@ export class Project { const nameNodes = references[r].getElementsByTagName(constants.DatabaseVariableLiteralValue); const name = nameNodes.length === 1 ? nameNodes[0].childNodes[0].nodeValue : undefined; - this.databaseReferences.push(new DacpacReferenceProjectEntry(Uri.file(filepath), name ? DatabaseReferenceLocation.differentDatabaseSameServer : DatabaseReferenceLocation.sameDatabase, name)); + + this.databaseReferences.push(new DacpacReferenceProjectEntry({ + dacpacFileLocation: Uri.file(utils.getPlatformSafeFileEntryPath(filepath)), + databaseLocation: name ? DatabaseReferenceLocation.differentDatabaseSameServer : DatabaseReferenceLocation.sameDatabase, + databaseName: name + })); } } @@ -285,21 +291,19 @@ export class Project { /** * Adds reference to the appropriate system database dacpac to the project */ - public async addSystemDatabaseReference(name: SystemDatabase): Promise { + public async addSystemDatabaseReference(settings: ISystemDatabaseReferenceSettings): Promise { let uri: Uri; let ssdtUri: Uri; - let dbName: string; - if (name === SystemDatabase.master) { + + if (settings.systemDb === SystemDatabase.master) { uri = this.getSystemDacpacUri(constants.masterDacpac); ssdtUri = this.getSystemDacpacSsdtUri(constants.masterDacpac); - dbName = constants.master; } else { uri = this.getSystemDacpacUri(constants.msdbDacpac); ssdtUri = this.getSystemDacpacSsdtUri(constants.msdbDacpac); - dbName = constants.msdb; } - let systemDatabaseReferenceProjectEntry = new SystemDatabaseReferenceProjectEntry(uri, ssdtUri, dbName); + const systemDatabaseReferenceProjectEntry = new SystemDatabaseReferenceProjectEntry(uri, ssdtUri, settings.databaseName); await this.addToProjFile(systemDatabaseReferenceProjectEntry); } @@ -340,8 +344,8 @@ export class Project { * @param uri Uri of the dacpac * @param databaseName name of the database */ - public async addDatabaseReference(uri: Uri, databaseLocation: DatabaseReferenceLocation, databaseName?: string): Promise { - let databaseReferenceEntry = new DacpacReferenceProjectEntry(uri, databaseLocation, databaseName); + public async addDatabaseReference(settings: IDacpacReferenceSettings): Promise { + const databaseReferenceEntry = new DacpacReferenceProjectEntry(settings); await this.addToProjFile(databaseReferenceEntry); } @@ -432,32 +436,41 @@ export class Project { throw new Error(constants.unableToFindObject(path, constants.folderObject)); } + private addSystemDatabaseReferenceToProjFile(entry: SystemDatabaseReferenceProjectEntry): void { + const systemDbReferenceNode = this.projFileXmlDoc.createElement(constants.ArtifactReference); + + // if it's a system database reference, we'll add an additional node with the SSDT location of the dacpac later + systemDbReferenceNode.setAttribute(constants.Condition, constants.NetCoreCondition); + systemDbReferenceNode.setAttribute(constants.Include, entry.pathForSqlProj()); + this.addDatabaseReferenceChildren(systemDbReferenceNode, entry); + this.findOrCreateItemGroup(constants.ArtifactReference).appendChild(systemDbReferenceNode); + this.databaseReferences.push(entry); + + // add a reference to the system dacpac in SSDT if it's a system db + const ssdtReferenceNode = this.projFileXmlDoc.createElement(constants.ArtifactReference); + ssdtReferenceNode.setAttribute(constants.Condition, constants.NotNetCoreCondition); + ssdtReferenceNode.setAttribute(constants.Include, entry.ssdtPathForSqlProj()); + this.addDatabaseReferenceChildren(ssdtReferenceNode, entry); + this.findOrCreateItemGroup(constants.ArtifactReference).appendChild(ssdtReferenceNode); + } + private addDatabaseReferenceToProjFile(entry: IDatabaseReferenceProjectEntry): void { // check if reference to this database already exists if (this.databaseReferenceExists(entry)) { throw new Error(constants.databaseReferenceAlreadyExists); } - let referenceNode = this.projFileXmlDoc.createElement(constants.ArtifactReference); const isSystemDatabaseProjectEntry = (entry).ssdtUri; - // if it's a system database reference, we'll add an additional node with the SSDT location of the dacpac later if (isSystemDatabaseProjectEntry) { - referenceNode.setAttribute(constants.Condition, constants.NetCoreCondition); - } + this.addSystemDatabaseReferenceToProjFile(entry); + } else { - referenceNode.setAttribute(constants.Include, entry.pathForSqlProj()); - this.addDatabaseReferenceChildren(referenceNode, entry.sqlCmdName); - this.findOrCreateItemGroup(constants.ArtifactReference).appendChild(referenceNode); - this.databaseReferences.push(entry); - - // add a reference to the system dacpac in SSDT if it's a system db - if (isSystemDatabaseProjectEntry) { - let ssdtReferenceNode = this.projFileXmlDoc.createElement(constants.ArtifactReference); - ssdtReferenceNode.setAttribute(constants.Condition, constants.NotNetCoreCondition); - ssdtReferenceNode.setAttribute(constants.Include, (entry).ssdtPathForSqlProj()); - this.addDatabaseReferenceChildren(ssdtReferenceNode, entry.sqlCmdName); - this.findOrCreateItemGroup(constants.ArtifactReference).appendChild(ssdtReferenceNode); + const referenceNode = this.projFileXmlDoc.createElement(constants.ArtifactReference); + referenceNode.setAttribute(constants.Include, entry.pathForSqlProj()); + this.addDatabaseReferenceChildren(referenceNode, entry); + this.findOrCreateItemGroup(constants.ArtifactReference).appendChild(referenceNode); + this.databaseReferences.push(entry); } } @@ -466,17 +479,19 @@ export class Project { return found; } - private addDatabaseReferenceChildren(referenceNode: any, name?: string): void { - let suppressMissingDependenciesErrorNode = this.projFileXmlDoc.createElement(constants.SuppressMissingDependenciesErrors); - let falseTextNode = this.projFileXmlDoc.createTextNode('False'); + private addDatabaseReferenceChildren(referenceNode: any, entry: IDatabaseReferenceProjectEntry): void { + // TODO: create checkbox for this setting + const suppressMissingDependenciesErrorNode = this.projFileXmlDoc.createElement(constants.SuppressMissingDependenciesErrors); + const falseTextNode = this.projFileXmlDoc.createTextNode('False'); suppressMissingDependenciesErrorNode.appendChild(falseTextNode); referenceNode.appendChild(suppressMissingDependenciesErrorNode); - if (name) { - let databaseVariableLiteralValue = this.projFileXmlDoc.createElement(constants.DatabaseVariableLiteralValue); - let databaseTextNode = this.projFileXmlDoc.createTextNode(name); - databaseVariableLiteralValue.appendChild(databaseTextNode); - referenceNode.appendChild(databaseVariableLiteralValue); + // TODO: add support for sqlcmd vars and server https://github.com/microsoft/azuredatastudio/issues/12036 + if (entry.databaseVariableLiteralValue) { + const databaseVariableLiteralValueElement = this.projFileXmlDoc.createElement(constants.DatabaseVariableLiteralValue); + const databaseTextNode = this.projFileXmlDoc.createTextNode(entry.databaseVariableLiteralValue); + databaseVariableLiteralValueElement.appendChild(databaseTextNode); + referenceNode.appendChild(databaseVariableLiteralValueElement); } } @@ -521,13 +536,22 @@ export class Project { return false; } + /** + * Update system db references to have the ADS and SSDT paths to the system dacpacs + */ public async updateSystemDatabaseReferencesInProjFile(): Promise { // find all system database references for (let r = 0; r < this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ArtifactReference).length; r++) { const currentNode = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ArtifactReference)[r]; if (!currentNode.getAttribute(constants.Condition) && currentNode.getAttribute(constants.Include).includes(constants.DacpacRootPath)) { // get name of system database - const name = currentNode.getAttribute(constants.Include).includes(constants.master) ? SystemDatabase.master : SystemDatabase.msdb; + const systemDb = currentNode.getAttribute(constants.Include).includes(constants.master) ? SystemDatabase.master : SystemDatabase.msdb; + + // get name + const nameNodes = currentNode.getElementsByTagName(constants.DatabaseVariableLiteralValue); + const databaseVariableName = nameNodes[0].childNodes[0]?.nodeValue; + + // remove this node this.projFileXmlDoc.documentElement.removeChild(currentNode); // delete ItemGroup if there aren't any other children @@ -536,9 +560,9 @@ export class Project { } // remove from database references because it'll get added again later - this.databaseReferences.splice(this.databaseReferences.findIndex(n => n.databaseName === (name === SystemDatabase.master ? constants.master : constants.msdb)), 1); + this.databaseReferences.splice(this.databaseReferences.findIndex(n => n.databaseName === (systemDb === SystemDatabase.master ? constants.master : constants.msdb)), 1); - await this.addSystemDatabaseReference(name); + await this.addSystemDatabaseReference({ databaseName: databaseVariableName, systemDb: systemDb }); } } } @@ -643,26 +667,37 @@ export class ProjectEntry { export interface IDatabaseReferenceProjectEntry extends ProjectEntry { databaseName: string; - sqlCmdName?: string | undefined; + databaseVariableLiteralValue?: string; } export class DacpacReferenceProjectEntry extends ProjectEntry implements IDatabaseReferenceProjectEntry { - sqlCmdName: string | undefined; + databaseLocation: DatabaseReferenceLocation; + databaseVariableLiteralValue?: string; - constructor(uri: Uri, public databaseLocation: DatabaseReferenceLocation, name?: string) { - super(uri, '', EntryType.DatabaseReference); - this.sqlCmdName = name; + constructor(settings: IDacpacReferenceSettings) { + super(settings.dacpacFileLocation, '', EntryType.DatabaseReference); + this.databaseLocation = settings.databaseLocation; + this.databaseVariableLiteralValue = settings.databaseName; } + /** + * File name that gets displayed in the project tree + */ public get databaseName(): string { return path.parse(utils.getPlatformSafeFileEntryPath(this.fsUri.fsPath)).name; } } -class SystemDatabaseReferenceProjectEntry extends DacpacReferenceProjectEntry { - constructor(uri: Uri, public ssdtUri: Uri, name: string) { - super(uri, DatabaseReferenceLocation.differentDatabaseSameServer, name); - this.sqlCmdName = name; +class SystemDatabaseReferenceProjectEntry extends ProjectEntry implements IDatabaseReferenceProjectEntry { + constructor(uri: Uri, public ssdtUri: Uri, public databaseVariableLiteralValue: string) { + super(uri, '', EntryType.DatabaseReference); + } + + /** + * File name that gets displayed in the project tree + */ + public get databaseName(): string { + return path.parse(utils.getPlatformSafeFileEntryPath(this.fsUri.fsPath)).name; } public pathForSqlProj(): string { diff --git a/extensions/sql-database-projects/src/test/project.test.ts b/extensions/sql-database-projects/src/test/project.test.ts index 1170158bd7..3e6b479c89 100644 --- a/extensions/sql-database-projects/src/test/project.test.ts +++ b/extensions/sql-database-projects/src/test/project.test.ts @@ -192,23 +192,28 @@ describe('Project: sqlproj content operations', function (): void { const project = await Project.openProject(projFilePath); should(project.databaseReferences.length).equal(0, 'There should be no datbase references to start with'); - await project.addSystemDatabaseReference(SystemDatabase.master); + await project.addSystemDatabaseReference({ databaseName: 'master', systemDb: SystemDatabase.master }); should(project.databaseReferences.length).equal(1, 'There should be one database reference after adding a reference to master'); should(project.databaseReferences[0].databaseName).equal(constants.master, 'The database reference should be master'); - // make sure reference to SSDT master dacpac was added + // make sure reference to ADS master dacpac and SSDT master dacpac was added let projFileText = (await fs.readFile(projFilePath)).toString(); + should(projFileText).containEql(convertSlashesForSqlProj(project.getSystemDacpacUri(constants.master).fsPath.substring(1))); should(projFileText).containEql(convertSlashesForSqlProj(project.getSystemDacpacSsdtUri(constants.master).fsPath.substring(1))); - await project.addSystemDatabaseReference(SystemDatabase.msdb); + await project.addSystemDatabaseReference({ databaseName: 'msdb', systemDb: SystemDatabase.msdb }); should(project.databaseReferences.length).equal(2, 'There should be two database references after adding a reference to msdb'); should(project.databaseReferences[1].databaseName).equal(constants.msdb, 'The database reference should be msdb'); - // make sure reference to SSDT msdb dacpac was added + // make sure reference to ADS msdb dacpac and SSDT msdb dacpac was added projFileText = (await fs.readFile(projFilePath)).toString(); + should(projFileText).containEql(convertSlashesForSqlProj(project.getSystemDacpacUri(constants.msdb).fsPath.substring(1))); should(projFileText).containEql(convertSlashesForSqlProj(project.getSystemDacpacSsdtUri(constants.msdb).fsPath.substring(1))); - await project.addDatabaseReference(Uri.parse('test.dacpac'), DatabaseReferenceLocation.sameDatabase); + await project.addDatabaseReference({ dacpacFileLocation: Uri.file('test.dacpac'), databaseLocation: DatabaseReferenceLocation.sameDatabase }); should(project.databaseReferences.length).equal(3, 'There should be three database references after adding a reference to test'); should(project.databaseReferences[2].databaseName).equal('test', 'The database reference should be test'); + // make sure reference to test.dacpac was added + projFileText = (await fs.readFile(projFilePath)).toString(); + should(projFileText).containEql('test.dacpac'); }); it('Should not allow adding duplicate database references', async function (): Promise { @@ -216,20 +221,20 @@ describe('Project: sqlproj content operations', function (): void { const project = await Project.openProject(projFilePath); should(project.databaseReferences.length).equal(0, 'There should be no database references to start with'); - await project.addSystemDatabaseReference(SystemDatabase.master); + await project.addSystemDatabaseReference({ databaseName: 'master', systemDb: SystemDatabase.master }); should(project.databaseReferences.length).equal(1, 'There should be one database reference after adding a reference to master'); should(project.databaseReferences[0].databaseName).equal(constants.master, 'project.databaseReferences[0].databaseName should be master'); // try to add reference to master again - await testUtils.shouldThrowSpecificError(async () => await project.addSystemDatabaseReference(SystemDatabase.master), constants.databaseReferenceAlreadyExists); + await testUtils.shouldThrowSpecificError(async () => await project.addSystemDatabaseReference({ databaseName: 'master', systemDb: SystemDatabase.master }), constants.databaseReferenceAlreadyExists); should(project.databaseReferences.length).equal(1, 'There should only be one database reference after trying to add a reference to master again'); - await project.addDatabaseReference(Uri.parse('test.dacpac'), DatabaseReferenceLocation.sameDatabase); + await project.addDatabaseReference({ dacpacFileLocation: Uri.file('test.dacpac'), databaseLocation: DatabaseReferenceLocation.sameDatabase }); should(project.databaseReferences.length).equal(2, 'There should be two database references after adding a reference to test.dacpac'); should(project.databaseReferences[1].databaseName).equal('test', 'project.databaseReferences[1].databaseName should be test'); // try to add reference to test.dacpac again - await testUtils.shouldThrowSpecificError(async () => await project.addDatabaseReference(Uri.parse('test.dacpac'), DatabaseReferenceLocation.sameDatabase), constants.databaseReferenceAlreadyExists); + await testUtils.shouldThrowSpecificError(async () => await project.addDatabaseReference({ dacpacFileLocation: Uri.file('test.dacpac'), databaseLocation: DatabaseReferenceLocation.sameDatabase }), constants.databaseReferenceAlreadyExists); should(project.databaseReferences.length).equal(2, 'There should be two database references after trying to add a reference to test.dacpac again'); }); diff --git a/extensions/sql-database-projects/src/test/projectController.test.ts b/extensions/sql-database-projects/src/test/projectController.test.ts index 8155705552..cde0e1d310 100644 --- a/extensions/sql-database-projects/src/test/projectController.test.ts +++ b/extensions/sql-database-projects/src/test/projectController.test.ts @@ -19,13 +19,15 @@ import { SqlDatabaseProjectTreeViewProvider } from '../controllers/databaseProje import { ProjectsController } from '../controllers/projectController'; import { promises as fs } from 'fs'; import { createContext, TestContext, mockDacFxResult } from './testContext'; -import { Project, SystemDatabase, ProjectEntry, reservedProjectFolders } from '../models/project'; +import { Project, ProjectEntry, reservedProjectFolders, SystemDatabase } from '../models/project'; import { PublishDatabaseDialog } from '../dialogs/publishDatabaseDialog'; import { IPublishSettings, IGenerateScriptSettings } from '../models/IPublishSettings'; import { exists } from '../common/utils'; import { ProjectRootTreeItem } from '../models/tree/projectTreeItem'; import { FolderNode, FileNode } from '../models/tree/fileFolderTreeItem'; import { BaseProjectTreeItem } from '../models/tree/baseTreeItem'; +import { AddDatabaseReferenceDialog } from '../dialogs/addDatabaseReferenceDialog'; +import { IDacpacReferenceSettings } from '../models/IDatabaseReferenceSettings'; let testContext: TestContext; @@ -473,117 +475,111 @@ describe('ProjectsController', function (): void { }); }); - describe('add database reference operations', function (): void { - it('Should show error when no reference type is selected', async function (): Promise { - sinon.stub(vscode.window, 'showQuickPick').resolves(undefined); - const spy = sinon.spy(vscode.window, 'showErrorMessage'); + describe('Add database reference', function (): void { + it('Add database reference dialog should open from ProjectController', async function (): Promise { + let opened = false; - const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); - await projController.addDatabaseReference(new Project('FakePath')); - should(spy.calledOnce).be.true('showErrorMessage should have been called exactly once'); - should(spy.calledWith(constants.databaseReferenceTypeRequired)).be.true(`showErrorMessage not called with expected message '${constants.databaseReferenceTypeRequired}' Actual '${spy.getCall(0).args[0]}'`); + let addDbReferenceDialog = TypeMoq.Mock.ofType(AddDatabaseReferenceDialog); + addDbReferenceDialog.setup(x => x.openDialog()).returns(() => { opened = true; return Promise.resolve(undefined) }); + + let projController = TypeMoq.Mock.ofType(ProjectsController); + projController.callBase = true; + projController.setup(x => x.getAddDatabaseReferenceDialog(TypeMoq.It.isAny())).returns(() => addDbReferenceDialog.object); + + await projController.object.addDatabaseReference(new Project('FakePath')); + should(opened).equal(true); }); - it('Should show error when no file is selected', async function (): Promise { - sinon.stub(vscode.window, 'showQuickPick').resolves({ label: constants.dacpac }); - sinon.stub(vscode.window, 'showOpenDialog').resolves(undefined); - const spy = sinon.spy(vscode.window, 'showErrorMessage'); + it('Callbacks are hooked up and called from Add database reference dialog', async function (): Promise { + const projPath = path.dirname(await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline)); + await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, projPath); + const proj = new Project(projPath); - const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); - await projController.addDatabaseReference(new Project('FakePath')); - should(spy.calledOnce).be.true('showErrorMessage should have been called exactly once'); - should(spy.calledWith(constants.dacpacFileLocationRequired)).be.true(`showErrorMessage not called with expected message '${constants.dacpacFileLocationRequired}' Actual '${spy.getCall(0).args[0]}'`); - }); + const addDbRefHoller = 'hello from callback for addDatabaseReference()'; - it('Should show error when no database name is provided', async function (): Promise { - sinon.stub(vscode.window, 'showInputBox').resolves(undefined); - sinon.stub(vscode.window, 'showQuickPick').resolves({ label: constants.dacpac }); - sinon.stub(vscode.window, 'showOpenDialog').resolves([vscode.Uri.file('FakePath')]); - const spy = sinon.spy(vscode.window, 'showErrorMessage'); + let holler = 'nothing'; - const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); - await projController.addDatabaseReference(new Project('FakePath')); - should(spy.calledOnce).be.true('showErrorMessage should have been called exactly once'); - should(spy.calledWith(constants.databaseNameRequired)).be.true(`showErrorMessage not called with expected message '${constants.databaseNameRequired}' Actual '${spy.getCall(0).args[0]}'`); - }); + const addDbReferenceDialog = TypeMoq.Mock.ofType(AddDatabaseReferenceDialog, undefined, undefined, proj); + addDbReferenceDialog.callBase = true; + addDbReferenceDialog.setup(x => x.addReferenceClick()).returns(() => { + projController.object.addDatabaseReferenceCallback(proj, { systemDb: SystemDatabase.master, databaseName: 'master' }); + return Promise.resolve(undefined); + }) - it('Should return the correct system database', async function (): Promise { - const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); - const projFilePath = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline); - const project = await Project.openProject(projFilePath); + const projController = TypeMoq.Mock.ofType(ProjectsController); + projController.callBase = true; + projController.setup(x => x.getAddDatabaseReferenceDialog(TypeMoq.It.isAny())).returns(() => addDbReferenceDialog.object); + projController.setup(x => x.addDatabaseReferenceCallback(TypeMoq.It.isAny(), TypeMoq.It.is((_): _ is IDacpacReferenceSettings => true))).returns(() => { + holler = addDbRefHoller; + return Promise.resolve(undefined); + }); - const stub = sinon.stub(vscode.window, 'showQuickPick').resolves({ label: constants.master }); - let systemDb = await projController.getSystemDatabaseName(project); - should.equal(systemDb, SystemDatabase.master); + let dialog = await projController.object.addDatabaseReference(proj); + await dialog.addReferenceClick(); - stub.resolves({ label: constants.msdb }); - systemDb = await projController.getSystemDatabaseName(project); - should.equal(systemDb, SystemDatabase.msdb); - - stub.resolves(undefined); - await testUtils.shouldThrowSpecificError(async () => await projController.getSystemDatabaseName(project), constants.systemDatabaseReferenceRequired); + should(holler).equal(addDbRefHoller, 'executionCallback() is supposed to have been setup and called for add database reference scenario'); }); }); +}); - describe.skip('ProjectsController: round trip feature with SSDT', function (): void { - it('Should show warning message for SSDT project opened in Azure Data Studio', async function (): Promise { - const stub = sinon.stub(vscode.window, 'showWarningMessage').returns(Promise.resolve(constants.noString)); +describe.skip('ProjectsController: round trip feature with SSDT', function (): void { + it('Should show warning message for SSDT project opened in Azure Data Studio', async function (): Promise { + const stub = sinon.stub(vscode.window, 'showWarningMessage').returns(Promise.resolve(constants.noString)); - // setup test files - const folderPath = await testUtils.generateTestFolderPath(); - const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.SSDTProjectFileBaseline, folderPath); - await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath); + // setup test files + const folderPath = await testUtils.generateTestFolderPath(); + const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.SSDTProjectFileBaseline, folderPath); + await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath); - const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); - await projController.openProject(vscode.Uri.file(sqlProjPath)); - should(stub.calledOnce).be.true('showWarningMessage should have been called exactly once'); - should(stub.calledWith(constants.updateProjectForRoundTrip)).be.true(`showWarningMessage not called with expected message '${constants.updateProjectForRoundTrip}' Actual '${stub.getCall(0).args[0]}'`); - }); + await projController.openProject(vscode.Uri.file(sqlProjPath)); + should(stub.calledOnce).be.true('showWarningMessage should have been called exactly once'); + should(stub.calledWith(constants.updateProjectForRoundTrip)).be.true(`showWarningMessage not called with expected message '${constants.updateProjectForRoundTrip}' Actual '${stub.getCall(0).args[0]}'`); + }); - it('Should not show warning message for non-SSDT projects that have the additional information for Build', async function (): Promise { - // setup test files - const folderPath = await testUtils.generateTestFolderPath(); - const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline, folderPath); - await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath); + it('Should not show warning message for non-SSDT projects that have the additional information for Build', async function (): Promise { + // setup test files + const folderPath = await testUtils.generateTestFolderPath(); + const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline, folderPath); + await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath); - const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); - const project = await projController.openProject(vscode.Uri.file(sqlProjPath)); // no error thrown + const project = await projController.openProject(vscode.Uri.file(sqlProjPath)); // no error thrown - should(project.importedTargets.length).equal(3); // additional target should exist by default - }); + should(project.importedTargets.length).equal(3); // additional target should exist by default + }); - it('Should not update project and no backup file should be created when update to project is rejected', async function (): Promise { - sinon.stub(vscode.window, 'showWarningMessage').returns(Promise.resolve(constants.noString)); - // setup test files - const folderPath = await testUtils.generateTestFolderPath(); - const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.SSDTProjectFileBaseline, folderPath); - await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath); + it('Should not update project and no backup file should be created when update to project is rejected', async function (): Promise { + sinon.stub(vscode.window, 'showWarningMessage').returns(Promise.resolve(constants.noString)); + // setup test files + const folderPath = await testUtils.generateTestFolderPath(); + const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.SSDTProjectFileBaseline, folderPath); + await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath); - const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); - const project = await projController.openProject(vscode.Uri.file(sqlProjPath)); + const project = await projController.openProject(vscode.Uri.file(sqlProjPath)); - should(await exists(sqlProjPath + '_backup')).equal(false); // backup file should not be generated - should(project.importedTargets.length).equal(2); // additional target should not be added by updateProjectForRoundTrip method - }); + should(await exists(sqlProjPath + '_backup')).equal(false); // backup file should not be generated + should(project.importedTargets.length).equal(2); // additional target should not be added by updateProjectForRoundTrip method + }); - it('Should load Project and associated import targets when update to project is accepted', async function (): Promise { - sinon.stub(vscode.window, 'showWarningMessage').returns(Promise.resolve(constants.yesString)); + it('Should load Project and associated import targets when update to project is accepted', async function (): Promise { + sinon.stub(vscode.window, 'showWarningMessage').returns(Promise.resolve(constants.yesString)); - // setup test files - const folderPath = await testUtils.generateTestFolderPath(); - const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.SSDTProjectFileBaseline, folderPath); - await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath); + // setup test files + const folderPath = await testUtils.generateTestFolderPath(); + const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.SSDTProjectFileBaseline, folderPath); + await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath); - const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); - const project = await projController.openProject(vscode.Uri.file(sqlProjPath)); + const project = await projController.openProject(vscode.Uri.file(sqlProjPath)); - should(await exists(sqlProjPath + '_backup')).equal(true); // backup file should be generated before the project is updated - should(project.importedTargets.length).equal(3); // additional target added by updateProjectForRoundTrip method - }); + should(await exists(sqlProjPath + '_backup')).equal(true); // backup file should be generated before the project is updated + should(project.importedTargets.length).equal(3); // additional target added by updateProjectForRoundTrip method }); });