From f49bfbc91bc7cea50b5dcff716bcec9d5c8f08d9 Mon Sep 17 00:00:00 2001 From: Sakshi Sharma <57200045+SakshiS-harma@users.noreply.github.com> Date: Wed, 27 May 2020 07:23:47 -0700 Subject: [PATCH] Round trip with SSDT (#10563) * Initial changes * Round trip feature implementation * Addressed PR comments * Addressed comments --- .../src/common/constants.ts | 25 ++++++ .../src/controllers/projectController.ts | 3 + .../src/models/project.ts | 88 +++++++++++++++++-- .../src/tools/netcoreTool.ts | 2 +- 4 files changed, 108 insertions(+), 10 deletions(-) diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index 81f163b08d..af4a6ce7dc 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -23,6 +23,8 @@ export const dataSourcesNodeName = localize('dataSourcesNodeName', "Data Sources export const sqlConnectionStringFriendly = localize('sqlConnectionStringFriendly', "SQL connection string"); export const newDatabaseProjectName = localize('newDatabaseProjectName', "New database project name:"); export const sqlDatabaseProject = localize('sqlDatabaseProject', "SQL database project"); +export const yesString = localize('yesString', "Yes"); +export const noString = localize('noString', "No"); export const extractTargetInput = localize('extractTargetInput', "Target for extraction:"); export const selectFileFolder = localize('selectFileFolder', "Select"); export function newObjectNamePrompt(objectType: string) { return localize('newObjectNamePrompt', 'New {0} name:', objectType); } @@ -60,6 +62,7 @@ export const projectLocationNotEmpty = localize('projectLocationNotEmpty', "Curr export const extractTargetRequired = localize('extractTargetRequired', "Target information for extract is required to import database to project."); export const schemaCompareNotInstalled = localize('schemaCompareNotInstalled', "Schema compare extension installation is required to run schema compare"); export const buildDacpacNotFound = localize('buildDacpacNotFound', "Dacpac created from build not found"); +export const updateProjectForRoundTrip = localize('updateProjectForRoundTrip', "To build this project, Azure Data Studio needs to update targets and references. If the project is created in SSDT, it will continue to work in both tools. Do you want Azure Data Studio to update the project?"); export function projectAlreadyOpened(path: string) { return localize('projectAlreadyOpened', "Project '{0}' is already opened.", path); } export function projectAlreadyExists(name: string, path: string) { return localize('projectAlreadyExists', "A project named {0} already exists in {1}.", name, path); } export function noFileExist(fileName: string) { return localize('noFileExist', "File {0} doesn't exist", fileName); } @@ -82,6 +85,28 @@ export const ItemGroup = 'ItemGroup'; export const Build = 'Build'; export const Folder = 'Folder'; export const Include = 'Include'; +export const Import = 'Import'; +export const Project = 'Project'; +export const Condition = 'Condition'; +export const PackageReference = 'PackageReference'; +export const Version = 'Version'; +export const PrivateAssets = 'PrivateAssets'; + + +// SqlProj File targets +export const NetCoreTargets = '$(NETCoreTargetsPath)\\Microsoft.Data.Tools.Schema.SqlTasks.targets'; +export const SqlDbTargets = '$(SQLDBExtensionsRefPath)\\Microsoft.Data.Tools.Schema.SqlTasks.targets'; +export const MsBuildtargets = '$(MSBuildExtensionsPath)\\Microsoft\\VisualStudio\\v$(VisualStudioVersion)\\SSDT\\Microsoft.Data.Tools.Schema.SqlTasks.targets'; +export const NetCoreCondition = '\'$(NetCoreBuild)\' == \'true\''; +export const SqlDbPresentCondition = '\'$(SQLDBExtensionsRefPath)\' != \'\''; +export const SqlDbNotPresentCondition = '\'$(SQLDBExtensionsRefPath)\' == \'\''; +export const RoundTripSqlDbPresentCondition = '\'$(NetCoreBuild)\' != \'true\' AND \'$(SQLDBExtensionsRefPath)\' != \'\''; +export const RoundTripSqlDbNotPresentCondition = '\'$(NetCoreBuild)\' != \'true\' AND \'$(SQLDBExtensionsRefPath)\' == \'\''; + +// SqlProj Reference Assembly Information +export const NETFrameworkAssembly = 'Microsoft.NETFramework.ReferenceAssemblies'; +export const VersionNumber = '1.0.0'; +export const All = 'All'; // SQL connection string components export const initialCatalogSetting = 'Initial Catalog'; diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index 5b3534a2d9..13918cb33a 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -71,6 +71,9 @@ export class ProjectsController { await newProject.readProjFile(); this.projects.push(newProject); + // Update for round tripping as needed + await newProject.updateProjectForRoundTrip(); + // Read datasources.json (if present) const dataSourcesFilePath = path.join(path.dirname(projectFile.fsPath), constants.dataSourcesFileName); diff --git a/extensions/sql-database-projects/src/models/project.ts b/extensions/sql-database-projects/src/models/project.ts index efea4b61e6..c907b79989 100644 --- a/extensions/sql-database-projects/src/models/project.ts +++ b/extensions/sql-database-projects/src/models/project.ts @@ -8,7 +8,7 @@ import * as xmldom from 'xmldom'; import * as constants from '../common/constants'; import * as utils from '../common/utils'; -import { Uri } from 'vscode'; +import { Uri, window } from 'vscode'; import { promises as fs } from 'fs'; import { DataSource } from './dataSources/dataSources'; @@ -20,6 +20,7 @@ export class Project { public projectFileName: string; public files: ProjectEntry[] = []; public dataSources: DataSource[] = []; + public importedTargets: string[] = []; public sqlCmdVariables: Record = {}; public get projectFolderPath() { @@ -41,7 +42,6 @@ export class Project { this.projFileXmlDoc = new xmldom.DOMParser().parseFromString(projFileText.toString()); // find all folders and files to include - for (let ig = 0; ig < this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ItemGroup).length; ig++) { const itemGroup = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ItemGroup)[ig]; @@ -53,6 +53,45 @@ export class Project { this.files.push(this.createProjectEntry(itemGroup.getElementsByTagName(constants.Folder)[f].getAttribute(constants.Include), EntryType.Folder)); } } + + // find all import statements to include + for (let i = 0; i < this.projFileXmlDoc.documentElement.getElementsByTagName(constants.Import).length; i++) { + const importTarget = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.Import)[i]; + this.importedTargets.push(importTarget.getAttribute(constants.Project)); + } + } + + public async updateProjectForRoundTrip() { + if (this.importedTargets.includes(constants.NetCoreTargets)) { + return; + } + + window.showWarningMessage(constants.updateProjectForRoundTrip, constants.yesString, constants.noString).then(async (result) => { + if (result === constants.yesString) { + await fs.copyFile(this.projectFilePath, this.projectFilePath + '_backup'); + await this.updateImportToSupportRoundTrip(); + await this.updatePackageReferenceInProjFile(); + } + }); + } + + private async updateImportToSupportRoundTrip(): Promise { + // update an SSDT project to include Net core target information + for (let i = 0; i < this.projFileXmlDoc.documentElement.getElementsByTagName(constants.Import).length; i++) { + const importTarget = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.Import)[i]; + + let condition = importTarget.getAttribute(constants.Condition); + let project = importTarget.getAttribute(constants.Project); + + if (condition === constants.SqlDbPresentCondition && project === constants.SqlDbTargets) { + await this.updateImportedTargetsToProjFile(constants.RoundTripSqlDbPresentCondition, project, importTarget); + } + if (condition === constants.SqlDbNotPresentCondition && project === constants.MsBuildtargets) { + await this.updateImportedTargetsToProjFile(constants.RoundTripSqlDbNotPresentCondition, project, importTarget); + } + } + + await this.updateImportedTargetsToProjFile(constants.NetCoreCondition, constants.NetCoreTargets, undefined); } /** @@ -109,14 +148,17 @@ export class Project { private findOrCreateItemGroup(containedTag?: string): any { let outputItemGroup = undefined; - // find any ItemGroup node that contains files; that's where we'll add - for (let i = 0; i < this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ItemGroup).length; i++) { - const currentItemGroup = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ItemGroup)[i]; + // search for a particular item goup if a child type is provided + if (containedTag) { + // find any ItemGroup node that contains files; that's where we'll add + for (let ig = 0; ig < this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ItemGroup).length; ig++) { + const currentItemGroup = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ItemGroup)[ig]; - // if we're not hunting for a particular child type, or if we are and we find it, use the ItemGroup - if (!containedTag || currentItemGroup.getElementsByTagName(containedTag).length > 0) { - outputItemGroup = currentItemGroup; - break; + // if we find the tag, use the ItemGroup + if (currentItemGroup.getElementsByTagName(containedTag).length > 0) { + outputItemGroup = currentItemGroup; + break; + } } } @@ -143,6 +185,34 @@ export class Project { this.findOrCreateItemGroup(constants.Folder).appendChild(newFolderNode); } + private async updateImportedTargetsToProjFile(condition: string, project: string, oldImportNode?: any): Promise { + const importNode = this.projFileXmlDoc.createElement(constants.Import); + importNode.setAttribute(constants.Condition, condition); + importNode.setAttribute(constants.Project, project); + + if (oldImportNode) { + this.projFileXmlDoc.documentElement.replaceChild(importNode, oldImportNode); + } + else { + this.projFileXmlDoc.documentElement.appendChild(importNode, oldImportNode); + } + + await this.serializeToProjFile(this.projFileXmlDoc); + return importNode; + } + + private async updatePackageReferenceInProjFile(): Promise { + const packageRefNode = this.projFileXmlDoc.createElement(constants.PackageReference); + packageRefNode.setAttribute(constants.Condition, constants.NetCoreCondition); + packageRefNode.setAttribute(constants.Include, constants.NETFrameworkAssembly); + packageRefNode.setAttribute(constants.Version, constants.VersionNumber); + packageRefNode.setAttribute(constants.PrivateAssets, constants.All); + + this.findOrCreateItemGroup(constants.PackageReference).appendChild(packageRefNode); + + await this.serializeToProjFile(this.projFileXmlDoc); + } + private async addToProjFile(entry: ProjectEntry) { switch (entry.type) { case EntryType.File: diff --git a/extensions/sql-database-projects/src/tools/netcoreTool.ts b/extensions/sql-database-projects/src/tools/netcoreTool.ts index 83e9d11c26..efe089aab8 100644 --- a/extensions/sql-database-projects/src/tools/netcoreTool.ts +++ b/extensions/sql-database-projects/src/tools/netcoreTool.ts @@ -112,7 +112,7 @@ export class NetCoreTool { } } - // spawns the dotnet command with aruments and redirects the error and output to ADS output channel + // spawns the dotnet command with arguments and redirects the error and output to ADS output channel public async runStreamedCommand(command: string, outputChannel: vscode.OutputChannel, options?: DotNetCommandOptions): Promise { const stdoutData: string[] = []; outputChannel.appendLine(` > ${command}`);