diff --git a/extensions/sql-database-projects/package.json b/extensions/sql-database-projects/package.json index c8787ee1e4..e38390426c 100644 --- a/extensions/sql-database-projects/package.json +++ b/extensions/sql-database-projects/package.json @@ -326,7 +326,7 @@ }, { "command": "sqlDatabaseProjects.delete", - "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.folder || viewItem == databaseProject.itemType.file", + "when": "view =~ /^(sqlDatabaseProjectsView|dataworkspace.views.main)$/ && viewItem == databaseProject.itemType.folder || viewItem == databaseProject.itemType.file || viewItem == databaseProject.itemType.reference", "group": "9_dbProjectsLast@2" }, { diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index 04651b629f..14a68cd380 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -68,7 +68,7 @@ export const reloadProject = localize('reloadProject', "Would you like to reload export function newObjectNamePrompt(objectType: string) { return localize('newObjectNamePrompt', 'New {0} name:', objectType); } export function deleteConfirmation(toDelete: string) { return localize('deleteConfirmation', "Are you sure you want to delete {0}?", toDelete); } export function deleteConfirmationContents(toDelete: string) { return localize('deleteConfirmationContents', "Are you sure you want to delete {0} and all of its contents?", toDelete); } - +export function deleteReferenceConfirmation(toDelete: string) { return localize('deleteReferenceConfirmation', "Are you sure you want to delete the reference to {0}?", toDelete); } // Publish dialog strings diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index 0bfc6cefcc..f587c2d489 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, reservedProjectFolders, FileProjectEntry, SqlProjectReferenceProjectEntry } from '../models/project'; +import { Project, reservedProjectFolders, FileProjectEntry, SqlProjectReferenceProjectEntry, IDatabaseReferenceProjectEntry } from '../models/project'; import { SqlDatabaseProjectTreeViewProvider } from './databaseProjectTreeViewProvider'; import { FolderNode, FileNode } from '../models/tree/fileFolderTreeItem'; import { IPublishSettings, IGenerateScriptSettings } from '../models/IPublishSettings'; @@ -29,6 +29,7 @@ import { BuildHelper } from '../tools/buildHelper'; import { PublishProfile, load } from '../models/publishProfile/publishProfile'; import { AddDatabaseReferenceDialog } from '../dialogs/addDatabaseReferenceDialog'; import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings, IProjectReferenceSettings } from '../models/IDatabaseReferenceSettings'; +import { DatabaseReferenceTreeItem } from '../models/tree/databaseReferencesTreeItem'; import { WorkspaceTreeItem } from 'dataworkspace'; /** @@ -393,7 +394,15 @@ export class ProjectsController { public async delete(context: BaseProjectTreeItem): Promise { const project = this.getProjectFromContext(context); - const confirmationPrompt = context instanceof FolderNode ? constants.deleteConfirmationContents(context.friendlyName) : constants.deleteConfirmation(context.friendlyName); + let confirmationPrompt; + if (context instanceof DatabaseReferenceTreeItem) { + confirmationPrompt = constants.deleteReferenceConfirmation(context.friendlyName); + } else if (context instanceof FolderNode) { + confirmationPrompt = constants.deleteConfirmationContents(context.friendlyName); + } else { + confirmationPrompt = constants.deleteConfirmation(context.friendlyName); + } + const response = await vscode.window.showWarningMessage(confirmationPrompt, { modal: true }, constants.yesString); if (response !== constants.yesString) { @@ -402,7 +411,14 @@ export class ProjectsController { let success = false; - if (context instanceof FileNode || FolderNode) { + if (context instanceof DatabaseReferenceTreeItem) { + const databaseReference = this.getDatabaseReference(project, context); + + if (databaseReference) { + await project.deleteDatabaseReference(databaseReference); + success = true; + } + } else if (context instanceof FileNode || FolderNode) { const fileEntry = this.getFileProjectEntry(project, context); if (fileEntry) { @@ -430,6 +446,17 @@ export class ProjectsController { return project.files.find(x => utils.getPlatformSafeFileEntryPath(x.relativePath) === utils.getPlatformSafeFileEntryPath(utils.trimUri(context.root.uri, context.uri))); } + private getDatabaseReference(project: Project, context: BaseProjectTreeItem): IDatabaseReferenceProjectEntry | undefined { + const root = context.root as ProjectRootTreeItem; + const databaseReference = context as DatabaseReferenceTreeItem; + + if (root && databaseReference) { + return project.databaseReferences.find(r => r.databaseName === databaseReference.treeItem.label); + } + + return undefined; + } + /** * Opens the folder containing the project * @param context a treeItem in a project's hierarchy, to be used to obtain a Project diff --git a/extensions/sql-database-projects/src/models/project.ts b/extensions/sql-database-projects/src/models/project.ts index 208ece35a5..7bbb7c650e 100644 --- a/extensions/sql-database-projects/src/models/project.ts +++ b/extensions/sql-database-projects/src/models/project.ts @@ -134,11 +134,20 @@ export class Project { const suppressMissingDependenciesErrorNode = references[r].getElementsByTagName(constants.SuppressMissingDependenciesErrors); const suppressMissingDependencies = suppressMissingDependenciesErrorNode[0].childNodes[0].nodeValue === true ?? false; - this.databaseReferences.push(new DacpacReferenceProjectEntry({ - dacpacFileLocation: Uri.file(utils.getPlatformSafeFileEntryPath(filepath)), - databaseName: name, - suppressMissingDependenciesErrors: suppressMissingDependencies - })); + const path = utils.convertSlashesForSqlProj(this.getSystemDacpacUri(`${name}.dacpac`).fsPath); + if (path.includes(filepath)) { + this.databaseReferences.push(new SystemDatabaseReferenceProjectEntry( + Uri.file(filepath), + this.getSystemDacpacSsdtUri(`${name}.dacpac`), + name, + suppressMissingDependencies)); + } else { + this.databaseReferences.push(new DacpacReferenceProjectEntry({ + dacpacFileLocation: Uri.file(utils.getPlatformSafeFileEntryPath(filepath)), + databaseName: name, + suppressMissingDependenciesErrors: suppressMissingDependencies + })); + } } } @@ -311,6 +320,11 @@ export class Project { await this.exclude(entry); } + public async deleteDatabaseReference(entry: IDatabaseReferenceProjectEntry): Promise { + await this.removeFromProjFile(entry); + this.databaseReferences = this.databaseReferences.filter(x => x !== entry); + } + /** * Set the compat level of the project * Just used in tests right now, but can be used later if this functionality is added to the UI @@ -520,6 +534,22 @@ export class Project { } } + private removeDatabaseReferenceFromProjFile(databaseReferenceEntry: IDatabaseReferenceProjectEntry): void { + const elementTag = databaseReferenceEntry instanceof SqlProjectReferenceProjectEntry ? constants.ProjectReference : constants.ArtifactReference; + const artifactReferenceNodes = this.projFileXmlDoc.documentElement.getElementsByTagName(elementTag); + const deleted = this.removeNode(databaseReferenceEntry.pathForSqlProj(), artifactReferenceNodes); + + // also delete SSDT reference if it's a system db reference + if (databaseReferenceEntry instanceof SystemDatabaseReferenceProjectEntry) { + const ssdtPath = databaseReferenceEntry.ssdtPathForSqlProj(); + this.removeNode(ssdtPath, artifactReferenceNodes); + } + + if (!deleted) { + throw new Error(constants.unableToFindSqlCmdVariable(databaseReferenceEntry.databaseName)); + } + } + private addSystemDatabaseReferenceToProjFile(entry: SystemDatabaseReferenceProjectEntry): void { const systemDbReferenceNode = this.projFileXmlDoc.createElement(constants.ArtifactReference); @@ -776,6 +806,7 @@ export class Project { this.removeFolderFromProjFile((entry).relativePath); break; case EntryType.DatabaseReference: + this.removeDatabaseReferenceFromProjFile(entry); break; case EntryType.SqlCmdVariable: this.removeSqlCmdVariableFromProjFile((entry).variableName); @@ -884,7 +915,7 @@ export class DacpacReferenceProjectEntry extends FileProjectEntry implements IDa } } -class SystemDatabaseReferenceProjectEntry extends FileProjectEntry implements IDatabaseReferenceProjectEntry { +export class SystemDatabaseReferenceProjectEntry extends FileProjectEntry implements IDatabaseReferenceProjectEntry { constructor(uri: Uri, public ssdtUri: Uri, public databaseVariableLiteralValue: string, public suppressMissingDependenciesErrors: boolean) { super(uri, '', EntryType.DatabaseReference); } diff --git a/extensions/sql-database-projects/src/test/project.test.ts b/extensions/sql-database-projects/src/test/project.test.ts index e8c3c13c35..c337d81e33 100644 --- a/extensions/sql-database-projects/src/test/project.test.ts +++ b/extensions/sql-database-projects/src/test/project.test.ts @@ -12,7 +12,7 @@ import * as testUtils from './testUtils'; import * as constants from '../common/constants'; import { promises as fs } from 'fs'; -import { Project, EntryType, TargetPlatform, SystemDatabase, DacpacReferenceProjectEntry, SqlProjectReferenceProjectEntry } from '../models/project'; +import { Project, EntryType, TargetPlatform, SystemDatabase, SystemDatabaseReferenceProjectEntry, SqlProjectReferenceProjectEntry } from '../models/project'; import { exists, convertSlashesForSqlProj } from '../common/utils'; import { Uri, window } from 'vscode'; @@ -47,7 +47,7 @@ describe('Project: sqlproj content operations', function (): void { // should only have one database reference even though there are two master.dacpac references (1 for ADS and 1 for SSDT) should(project.databaseReferences.length).equal(1); should(project.databaseReferences[0].databaseName).containEql(constants.master); - should(project.databaseReferences[0] instanceof DacpacReferenceProjectEntry).equal(true); + should(project.databaseReferences[0] instanceof SystemDatabaseReferenceProjectEntry).equal(true); // Pre-post deployment scripts should(project.preDeployScripts.length).equal(1); @@ -67,7 +67,7 @@ describe('Project: sqlproj content operations', function (): void { // should only have two database references even though there are two master.dacpac references (1 for ADS and 1 for SSDT) should(project.databaseReferences.length).equal(2); should(project.databaseReferences[0].databaseName).containEql(constants.master); - should(project.databaseReferences[0] instanceof DacpacReferenceProjectEntry).equal(true); + should(project.databaseReferences[0] instanceof SystemDatabaseReferenceProjectEntry).equal(true); should(project.databaseReferences[1].databaseName).containEql('TestProjectName'); should(project.databaseReferences[1] instanceof SqlProjectReferenceProjectEntry).equal(true); }); diff --git a/extensions/sql-database-projects/src/test/projectController.test.ts b/extensions/sql-database-projects/src/test/projectController.test.ts index 588674bdba..f721480d40 100644 --- a/extensions/sql-database-projects/src/test/projectController.test.ts +++ b/extensions/sql-database-projects/src/test/projectController.test.ts @@ -236,6 +236,39 @@ describe('ProjectsController', function (): void { should(await exists(noneEntry.fsUri.fsPath)).equal(false, 'none entry pre-deployment script is supposed to be deleted'); }); + it('Should delete database references', async function (): Promise { + // setup - openProject baseline has a system db reference to master + const proj = await testUtils.createTestProject(baselines.openProjectFileBaseline); + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + sinon.stub(vscode.window, 'showWarningMessage').returns(Promise.resolve(constants.yesString)); + + // add dacpac reference + proj.addDatabaseReference({ + dacpacFileLocation: vscode.Uri.file('test2.dacpac'), + databaseName: 'test2DbName', + databaseVariable: 'test2Db', + suppressMissingDependenciesErrors: false + }); + // add project reference + proj.addProjectReference({ + projectName: 'project1', + projectGuid: '', + projectRelativePath: vscode.Uri.file(path.join('..','project1', 'project1.sqlproj')), + suppressMissingDependenciesErrors: false + }); + + const projTreeRoot = new ProjectRootTreeItem(proj); + should(proj.databaseReferences.length).equal(3, 'Should start with 3 database references'); + + const databaseReferenceNodeChildren = projTreeRoot.children.find(x => x.friendlyName === constants.databaseReferencesNodeName)?.children; + await projController.delete(databaseReferenceNodeChildren?.find(x => x.friendlyName === 'master')!); // system db reference + await projController.delete(databaseReferenceNodeChildren?.find(x => x.friendlyName === 'test2')!); // dacpac reference + await projController.delete(databaseReferenceNodeChildren?.find(x => x.friendlyName === 'project1')!); // project reference + + // confirm result + should(proj.databaseReferences.length).equal(0, 'All database references should have been deleted'); + }); + it('Should exclude nested ProjectEntry from node', async function (): Promise { let proj = await testUtils.createTestProject(templates.newSqlProjectTemplate); const setupResult = await setupDeleteExcludeTest(proj);