Delete database reference (#12531)

* remove ItemGroup if node being removed is the only one

* fix for if ItemGroup has elements with different tag names

* fix for ItemGroups not at the end of the sqlproj

* add delete for db references

* fix failing tests

* add test

* cleanup

* Addressing comments and fixing a string
This commit is contained in:
Kim Santiago
2020-09-24 17:27:13 -07:00
committed by GitHub
parent 9780eebb12
commit 49de1f80cf
6 changed files with 105 additions and 14 deletions

View File

@@ -326,7 +326,7 @@
}, },
{ {
"command": "sqlDatabaseProjects.delete", "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" "group": "9_dbProjectsLast@2"
}, },
{ {

View File

@@ -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 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 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 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 // Publish dialog strings

View File

@@ -17,7 +17,7 @@ import * as azdata from 'azdata';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { PublishDatabaseDialog } from '../dialogs/publishDatabaseDialog'; 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 { SqlDatabaseProjectTreeViewProvider } from './databaseProjectTreeViewProvider';
import { FolderNode, FileNode } from '../models/tree/fileFolderTreeItem'; import { FolderNode, FileNode } from '../models/tree/fileFolderTreeItem';
import { IPublishSettings, IGenerateScriptSettings } from '../models/IPublishSettings'; import { IPublishSettings, IGenerateScriptSettings } from '../models/IPublishSettings';
@@ -29,6 +29,7 @@ import { BuildHelper } from '../tools/buildHelper';
import { PublishProfile, load } from '../models/publishProfile/publishProfile'; import { PublishProfile, load } from '../models/publishProfile/publishProfile';
import { AddDatabaseReferenceDialog } from '../dialogs/addDatabaseReferenceDialog'; import { AddDatabaseReferenceDialog } from '../dialogs/addDatabaseReferenceDialog';
import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings, IProjectReferenceSettings } from '../models/IDatabaseReferenceSettings'; import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings, IProjectReferenceSettings } from '../models/IDatabaseReferenceSettings';
import { DatabaseReferenceTreeItem } from '../models/tree/databaseReferencesTreeItem';
import { WorkspaceTreeItem } from 'dataworkspace'; import { WorkspaceTreeItem } from 'dataworkspace';
/** /**
@@ -393,7 +394,15 @@ export class ProjectsController {
public async delete(context: BaseProjectTreeItem): Promise<void> { public async delete(context: BaseProjectTreeItem): Promise<void> {
const project = this.getProjectFromContext(context); 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); const response = await vscode.window.showWarningMessage(confirmationPrompt, { modal: true }, constants.yesString);
if (response !== constants.yesString) { if (response !== constants.yesString) {
@@ -402,7 +411,14 @@ export class ProjectsController {
let success = false; 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); const fileEntry = this.getFileProjectEntry(project, context);
if (fileEntry) { 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))); 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 * Opens the folder containing the project
* @param context a treeItem in a project's hierarchy, to be used to obtain a Project * @param context a treeItem in a project's hierarchy, to be used to obtain a Project

View File

@@ -134,6 +134,14 @@ export class Project {
const suppressMissingDependenciesErrorNode = references[r].getElementsByTagName(constants.SuppressMissingDependenciesErrors); const suppressMissingDependenciesErrorNode = references[r].getElementsByTagName(constants.SuppressMissingDependenciesErrors);
const suppressMissingDependencies = suppressMissingDependenciesErrorNode[0].childNodes[0].nodeValue === true ?? false; const suppressMissingDependencies = suppressMissingDependenciesErrorNode[0].childNodes[0].nodeValue === true ?? false;
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({ this.databaseReferences.push(new DacpacReferenceProjectEntry({
dacpacFileLocation: Uri.file(utils.getPlatformSafeFileEntryPath(filepath)), dacpacFileLocation: Uri.file(utils.getPlatformSafeFileEntryPath(filepath)),
databaseName: name, databaseName: name,
@@ -141,6 +149,7 @@ export class Project {
})); }));
} }
} }
}
// find project references // find project references
const projectReferences = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ProjectReference); const projectReferences = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ProjectReference);
@@ -311,6 +320,11 @@ export class Project {
await this.exclude(entry); await this.exclude(entry);
} }
public async deleteDatabaseReference(entry: IDatabaseReferenceProjectEntry): Promise<void> {
await this.removeFromProjFile(entry);
this.databaseReferences = this.databaseReferences.filter(x => x !== entry);
}
/** /**
* Set the compat level of the project * 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 * 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 { private addSystemDatabaseReferenceToProjFile(entry: SystemDatabaseReferenceProjectEntry): void {
const systemDbReferenceNode = this.projFileXmlDoc.createElement(constants.ArtifactReference); const systemDbReferenceNode = this.projFileXmlDoc.createElement(constants.ArtifactReference);
@@ -776,6 +806,7 @@ export class Project {
this.removeFolderFromProjFile((<FileProjectEntry>entry).relativePath); this.removeFolderFromProjFile((<FileProjectEntry>entry).relativePath);
break; break;
case EntryType.DatabaseReference: case EntryType.DatabaseReference:
this.removeDatabaseReferenceFromProjFile(<IDatabaseReferenceProjectEntry>entry);
break; break;
case EntryType.SqlCmdVariable: case EntryType.SqlCmdVariable:
this.removeSqlCmdVariableFromProjFile((<SqlCmdVariableProjectEntry>entry).variableName); this.removeSqlCmdVariableFromProjFile((<SqlCmdVariableProjectEntry>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) { constructor(uri: Uri, public ssdtUri: Uri, public databaseVariableLiteralValue: string, public suppressMissingDependenciesErrors: boolean) {
super(uri, '', EntryType.DatabaseReference); super(uri, '', EntryType.DatabaseReference);
} }

View File

@@ -12,7 +12,7 @@ import * as testUtils from './testUtils';
import * as constants from '../common/constants'; import * as constants from '../common/constants';
import { promises as fs } from 'fs'; 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 { exists, convertSlashesForSqlProj } from '../common/utils';
import { Uri, window } from 'vscode'; 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 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.length).equal(1);
should(project.databaseReferences[0].databaseName).containEql(constants.master); 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 // Pre-post deployment scripts
should(project.preDeployScripts.length).equal(1); 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 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.length).equal(2);
should(project.databaseReferences[0].databaseName).containEql(constants.master); 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].databaseName).containEql('TestProjectName');
should(project.databaseReferences[1] instanceof SqlProjectReferenceProjectEntry).equal(true); should(project.databaseReferences[1] instanceof SqlProjectReferenceProjectEntry).equal(true);
}); });

View File

@@ -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'); 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<void> {
// 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(<any>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<void> { it('Should exclude nested ProjectEntry from node', async function (): Promise<void> {
let proj = await testUtils.createTestProject(templates.newSqlProjectTemplate); let proj = await testUtils.createTestProject(templates.newSqlProjectTemplate);
const setupResult = await setupDeleteExcludeTest(proj); const setupResult = await setupDeleteExcludeTest(proj);