diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index 01d921f293..988de17405 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -417,7 +417,8 @@ export class ProjectsController { if (root && fileOrFolder) { // use relative path and not tree paths for files and folder - return project.files.find(x => utils.getPlatformSafeFileEntryPath(x.relativePath) === utils.getPlatformSafeFileEntryPath(utils.trimUri(root.fileSystemUri, fileOrFolder.fileSystemUri))); + const allFileEntries = project.files.concat(project.preDeployScripts).concat(project.postDeployScripts).concat(project.noneDeployScripts); + return allFileEntries.find(x => utils.getPlatformSafeFileEntryPath(x.relativePath) === utils.getPlatformSafeFileEntryPath(utils.trimUri(root.fileSystemUri, fileOrFolder.fileSystemUri))); } return project.files.find(x => utils.getPlatformSafeFileEntryPath(x.relativePath) === utils.getPlatformSafeFileEntryPath(utils.trimUri(context.root.uri, context.uri))); } diff --git a/extensions/sql-database-projects/src/models/project.ts b/extensions/sql-database-projects/src/models/project.ts index 56febc2541..e1ad09d81c 100644 --- a/extensions/sql-database-projects/src/models/project.ts +++ b/extensions/sql-database-projects/src/models/project.ts @@ -263,11 +263,11 @@ export class Project { switch (itemType) { case templates.preDeployScript: xmlTag = constants.PreDeploy; - this.preDeployScripts.push(fileEntry); + this.preDeployScripts.length === 0 ? this.preDeployScripts.push(fileEntry) : this.noneDeployScripts.push(fileEntry); break; case templates.postDeployScript: xmlTag = constants.PostDeploy; - this.postDeployScripts.push(fileEntry); + this.postDeployScripts.length === 0 ? this.postDeployScripts.push(fileEntry) : this.noneDeployScripts.push(fileEntry); break; default: xmlTag = constants.Build; @@ -280,14 +280,18 @@ export class Project { } public async exclude(entry: FileProjectEntry): Promise { - const toExclude: FileProjectEntry[] = this.files.filter(x => x.fsUri.fsPath.startsWith(entry.fsUri.fsPath)); + const toExclude: FileProjectEntry[] = this.files.concat(this.preDeployScripts).concat(this.postDeployScripts).concat(this.noneDeployScripts).filter(x => x.fsUri.fsPath.startsWith(entry.fsUri.fsPath)); await this.removeFromProjFile(toExclude); + this.files = this.files.filter(x => !x.fsUri.fsPath.startsWith(entry.fsUri.fsPath)); + this.preDeployScripts = this.preDeployScripts.filter(x => !x.fsUri.fsPath.startsWith(entry.fsUri.fsPath)); + this.postDeployScripts = this.postDeployScripts.filter(x => !x.fsUri.fsPath.startsWith(entry.fsUri.fsPath)); + this.noneDeployScripts = this.noneDeployScripts.filter(x => !x.fsUri.fsPath.startsWith(entry.fsUri.fsPath)); } public async deleteFileFolder(entry: FileProjectEntry): Promise { // compile a list of folder contents to delete; if entry is a file, contents will contain only itself - const toDeleteFiles: FileProjectEntry[] = this.files.filter(x => x.fsUri.fsPath.startsWith(entry.fsUri.fsPath) && x.type === EntryType.File); + const toDeleteFiles: FileProjectEntry[] = this.files.concat(this.preDeployScripts).concat(this.postDeployScripts).concat(this.noneDeployScripts).filter(x => x.fsUri.fsPath.startsWith(entry.fsUri.fsPath) && x.type === EntryType.File); const toDeleteFolders: FileProjectEntry[] = this.files.filter(x => x.fsUri.fsPath.startsWith(entry.fsUri.fsPath) && x.type === EntryType.Folder).sort(x => -x.relativePath.length); await Promise.all(toDeleteFiles.map(x => fs.unlink(x.fsUri.fsPath))); @@ -436,6 +440,9 @@ export class Project { private removeFileFromProjFile(path: string): void { const fileNodes = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.Build); + const preDeployNodes = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.PreDeploy); + const postDeployNodes = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.PostDeploy); + const noneNodes = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.None); for (let i = 0; i < fileNodes.length; i++) { if (fileNodes[i].getAttribute(constants.Include) === utils.convertSlashesForSqlProj(path)) { @@ -444,6 +451,27 @@ export class Project { } } + for (let i = 0; i < preDeployNodes.length; i++) { + if (preDeployNodes[i].getAttribute(constants.Include) === utils.convertSlashesForSqlProj(path)) { + preDeployNodes[i].parentNode.removeChild(preDeployNodes[i]); + return; + } + } + + for (let i = 0; i < postDeployNodes.length; i++) { + if (postDeployNodes[i].getAttribute(constants.Include) === utils.convertSlashesForSqlProj(path)) { + postDeployNodes[i].parentNode.removeChild(postDeployNodes[i]); + return; + } + } + + for (let i = 0; i < noneNodes.length; i++) { + if (noneNodes[i].getAttribute(constants.Include) === utils.convertSlashesForSqlProj(path)) { + noneNodes[i].parentNode.removeChild(noneNodes[i]); + return; + } + } + throw new Error(constants.unableToFindObject(path, constants.fileObject)); } diff --git a/extensions/sql-database-projects/src/test/projectController.test.ts b/extensions/sql-database-projects/src/test/projectController.test.ts index eb4be54e50..7e373b808d 100644 --- a/extensions/sql-database-projects/src/test/projectController.test.ts +++ b/extensions/sql-database-projects/src/test/projectController.test.ts @@ -211,39 +211,57 @@ describe('ProjectsController', function (): void { it('Should delete nested ProjectEntry from node', async function (): Promise { let proj = await testUtils.createTestProject(templates.newSqlProjectTemplate); const setupResult = await setupDeleteExcludeTest(proj); - const scriptEntry = setupResult[0], projTreeRoot = setupResult[1]; + const scriptEntry = setupResult[0], projTreeRoot = setupResult[1], preDeployEntry = setupResult[2], postDeployEntry = setupResult[3], noneEntry = setupResult[4]; const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); await projController.delete(projTreeRoot.children.find(x => x.friendlyName === 'UpperFolder')!.children[0] /* LowerFolder */); await projController.delete(projTreeRoot.children.find(x => x.friendlyName === 'anotherScript.sql')!); + await projController.delete(projTreeRoot.children.find(x => x.friendlyName === 'Script.PreDeployment1.sql')!); + await projController.delete(projTreeRoot.children.find(x => x.friendlyName === 'Script.PreDeployment2.sql')!); + await projController.delete(projTreeRoot.children.find(x => x.friendlyName === 'Script.PostDeployment1.sql')!); proj = await Project.openProject(proj.projectFilePath); // reload edited sqlproj from disk // confirm result should(proj.files.length).equal(1, 'number of file/folder entries'); // lowerEntry and the contained scripts should be deleted should(proj.files[0].relativePath).equal('UpperFolder'); + should(proj.preDeployScripts.length).equal(0); + should(proj.postDeployScripts.length).equal(0); + should(proj.noneDeployScripts.length).equal(0); should(await exists(scriptEntry.fsUri.fsPath)).equal(false, 'script is supposed to be deleted'); + should(await exists(preDeployEntry.fsUri.fsPath)).equal(false, 'pre-deployment script is supposed to be deleted'); + should(await exists(postDeployEntry.fsUri.fsPath)).equal(false, 'post-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 exclude nested ProjectEntry from node', async function (): Promise { let proj = await testUtils.createTestProject(templates.newSqlProjectTemplate); const setupResult = await setupDeleteExcludeTest(proj); - const scriptEntry = setupResult[0], projTreeRoot = setupResult[1]; + const scriptEntry = setupResult[0], projTreeRoot = setupResult[1], preDeployEntry = setupResult[2], postDeployEntry = setupResult[3], noneEntry = setupResult[4]; const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); await projController.exclude(projTreeRoot.children.find(x => x.friendlyName === 'UpperFolder')!.children[0] /* LowerFolder */); await projController.exclude(projTreeRoot.children.find(x => x.friendlyName === 'anotherScript.sql')!); + await projController.exclude(projTreeRoot.children.find(x => x.friendlyName === 'Script.PreDeployment1.sql')!); + await projController.exclude(projTreeRoot.children.find(x => x.friendlyName === 'Script.PreDeployment2.sql')!); + await projController.exclude(projTreeRoot.children.find(x => x.friendlyName === 'Script.PostDeployment1.sql')!); proj = await Project.openProject(proj.projectFilePath); // reload edited sqlproj from disk // confirm result should(proj.files.length).equal(1, 'number of file/folder entries'); // LowerFolder and the contained scripts should be deleted should(proj.files[0].relativePath).equal('UpperFolder'); // UpperFolder should still be there + should(proj.preDeployScripts.length).equal(0); + should(proj.postDeployScripts.length).equal(0); + should(proj.noneDeployScripts.length).equal(0); should(await exists(scriptEntry.fsUri.fsPath)).equal(true, 'script is supposed to still exist on disk'); + should(await exists(preDeployEntry.fsUri.fsPath)).equal(true, 'pre-deployment script is supposed to still exist on disk'); + should(await exists(postDeployEntry.fsUri.fsPath)).equal(true, 'post-deployment script is supposed to still exist on disk'); + should(await exists(noneEntry.fsUri.fsPath)).equal(true, 'none entry pre-deployment script is supposed to still exist on disk'); }); it('Should reload correctly after changing sqlproj file', async function (): Promise { @@ -604,20 +622,26 @@ describe.skip('ProjectsController: round trip feature with SSDT', function (): v }); -async function setupDeleteExcludeTest(proj: Project): Promise<[FileProjectEntry, ProjectRootTreeItem]> { +async function setupDeleteExcludeTest(proj: Project): Promise<[FileProjectEntry, ProjectRootTreeItem, FileProjectEntry, FileProjectEntry, FileProjectEntry]> { await proj.addFolderItem('UpperFolder'); await proj.addFolderItem('UpperFolder/LowerFolder'); const scriptEntry = await proj.addScriptItem('UpperFolder/LowerFolder/someScript.sql', 'not a real script'); await proj.addScriptItem('UpperFolder/LowerFolder/someOtherScript.sql', 'Also not a real script'); await proj.addScriptItem('../anotherScript.sql', 'Also not a real script'); + const preDeployEntry = await proj.addScriptItem('Script.PreDeployment1.sql', 'pre-deployment stuff', templates.preDeployScript); + const noneEntry = await proj.addScriptItem('Script.PreDeployment2.sql', 'more pre-deployment stuff', templates.preDeployScript); + const postDeployEntry = await proj.addScriptItem('Script.PostDeployment1.sql', 'post-deployment stuff', templates.postDeployScript); const projTreeRoot = new ProjectRootTreeItem(proj); sinon.stub(vscode.window, 'showWarningMessage').returns(Promise.resolve(constants.yesString)); // confirm setup should(proj.files.length).equal(5, 'number of file/folder entries'); + should(proj.preDeployScripts.length).equal(1, 'number of pre-deployment script entries'); + should(proj.postDeployScripts.length).equal(1, 'number of post-deployment script entries'); + should(proj.noneDeployScripts.length).equal(1, 'number of none script entries'); should(path.parse(scriptEntry.fsUri.fsPath).base).equal('someScript.sql'); should((await fs.readFile(scriptEntry.fsUri.fsPath)).toString()).equal('not a real script'); - return [scriptEntry, projTreeRoot]; + return [scriptEntry, projTreeRoot, preDeployEntry, postDeployEntry, noneEntry]; }