diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index ffc17bc93b..5ed000bc8c 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -26,6 +26,7 @@ export const databaseSchemaProvider = 'DatabaseSchemaProvider'; export const projectNodeName = localize('projectNodeName', "Database Project"); export const dataSourcesNodeName = localize('dataSourcesNodeName', "Data Sources"); +export const databaseReferencesNodeName = localize('databaseReferencesNodeName', "Database References"); 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"); @@ -83,6 +84,7 @@ export const dacpacFileLocationRequired = localize('dacpacFileLocationRequired', export const databaseLocationRequired = localize('databaseLocation', "Database location is required for adding a reference to a database"); export const databaseNameRequired = localize('databaseNameRequired', "Database name is required for adding a reference to a different database"); export const invalidDataSchemaProvider = localize('invalidDataSchemaProvider', "Invalid DSP in .sqlproj file"); +export const invalidDatabaseReference = localize('invalidDatabaseReference', "Invalid database reference in .sqlproj file"); export const databaseSelectionRequired = localize('databaseSelectionRequired', "Database selection is required to import a 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); } diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index abf734bd65..4207b7c9a6 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -321,6 +321,8 @@ export class ProjectsController { await project.addDatabaseReference(dacpacFileLocation, databaseLocation, false); } } + + this.refreshProjectsTree(); } catch (err) { this.apiWrapper.showErrorMessage(utils.getErrorMessage(err)); } diff --git a/extensions/sql-database-projects/src/models/project.ts b/extensions/sql-database-projects/src/models/project.ts index fc898ca4df..8bd505c5a8 100644 --- a/extensions/sql-database-projects/src/models/project.ts +++ b/extensions/sql-database-projects/src/models/project.ts @@ -22,6 +22,7 @@ export class Project { public files: ProjectEntry[] = []; public dataSources: DataSource[] = []; public importedTargets: string[] = []; + public databaseReferences: string[] = []; public sqlCmdVariables: Record = {}; public get projectFolderPath() { @@ -60,6 +61,16 @@ export class Project { const importTarget = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.Import)[i]; this.importedTargets.push(importTarget.getAttribute(constants.Project)); } + + // find all database references to include + for (let r = 0; r < this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ArtifactReference).length; r++) { + const filepath = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ArtifactReference)[r].getAttribute(constants.Include); + if (!filepath) { + throw new Error(constants.invalidDatabaseReference); + } + + this.databaseReferences.push(path.parse(filepath).name); + } } public async updateProjectForRoundTrip() { @@ -259,7 +270,8 @@ export class Project { referenceNode.appendChild(databaseVariableLiteralValue); } - this.findOrCreateItemGroup().appendChild(referenceNode); + this.findOrCreateItemGroup(constants.ArtifactReference).appendChild(referenceNode); + this.databaseReferences.push(path.parse(entry.fsUri.fsPath.toString()).name); } private async updateImportedTargetsToProjFile(condition: string, projectAttributeVal: string, oldImportNode?: any): Promise { diff --git a/extensions/sql-database-projects/src/models/tree/databaseReferencesTreeItem.ts b/extensions/sql-database-projects/src/models/tree/databaseReferencesTreeItem.ts new file mode 100644 index 0000000000..a5c380073c --- /dev/null +++ b/extensions/sql-database-projects/src/models/tree/databaseReferencesTreeItem.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as constants from '../../common/constants'; +import { BaseProjectTreeItem, MessageTreeItem } from './baseTreeItem'; +import { ProjectRootTreeItem } from './projectTreeItem'; + +/** + * Folder for containing references nodes in the tree + */ +export class DatabaseReferencesTreeItem extends BaseProjectTreeItem { + private references: MessageTreeItem[] = []; + + constructor(project: ProjectRootTreeItem) { + super(vscode.Uri.file(path.join(project.uri.path, constants.databaseReferencesNodeName)), project); + + this.construct(); + } + + private construct() { + for (const reference of (this.parent as ProjectRootTreeItem).project.databaseReferences) { + this.references.push(new MessageTreeItem(reference)); + } + } + + public get children(): BaseProjectTreeItem[] { + return this.references; + } + + public get treeItem(): vscode.TreeItem { + return new vscode.TreeItem(this.uri, vscode.TreeItemCollapsibleState.Collapsed); + } +} diff --git a/extensions/sql-database-projects/src/models/tree/projectTreeItem.ts b/extensions/sql-database-projects/src/models/tree/projectTreeItem.ts index b7181b3576..d4980097ae 100644 --- a/extensions/sql-database-projects/src/models/tree/projectTreeItem.ts +++ b/extensions/sql-database-projects/src/models/tree/projectTreeItem.ts @@ -10,12 +10,14 @@ import { BaseProjectTreeItem } from './baseTreeItem'; import * as fileTree from './fileFolderTreeItem'; import { Project, ProjectEntry, EntryType } from '../project'; import * as utils from '../../common/utils'; +import { DatabaseReferencesTreeItem } from './databaseReferencesTreeItem'; /** * TreeNode root that represents an entire project */ export class ProjectRootTreeItem extends BaseProjectTreeItem { dataSourceNode: DataSourcesTreeItem; + databaseReferencesNode: DatabaseReferencesTreeItem; fileChildren: { [childName: string]: (fileTree.FolderNode | fileTree.FileNode) } = {}; project: Project; @@ -24,6 +26,7 @@ export class ProjectRootTreeItem extends BaseProjectTreeItem { this.project = project; this.dataSourceNode = new DataSourcesTreeItem(this); + this.databaseReferencesNode = new DatabaseReferencesTreeItem(this); this.construct(); } @@ -31,6 +34,7 @@ export class ProjectRootTreeItem extends BaseProjectTreeItem { public get children(): BaseProjectTreeItem[] { const output: BaseProjectTreeItem[] = []; output.push(this.dataSourceNode); + output.push(this.databaseReferencesNode); return output.concat(Object.values(this.fileChildren).sort(fileTree.sortFileFolderNodes)); } diff --git a/extensions/sql-database-projects/src/test/baselines/openSqlProjectBaseline.xml b/extensions/sql-database-projects/src/test/baselines/openSqlProjectBaseline.xml index d0995e66f3..86b4eeaeb0 100644 --- a/extensions/sql-database-projects/src/test/baselines/openSqlProjectBaseline.xml +++ b/extensions/sql-database-projects/src/test/baselines/openSqlProjectBaseline.xml @@ -73,4 +73,10 @@ + + + False + master + + diff --git a/extensions/sql-database-projects/src/test/project.test.ts b/extensions/sql-database-projects/src/test/project.test.ts index 73204b92a6..a1cdd69664 100644 --- a/extensions/sql-database-projects/src/test/project.test.ts +++ b/extensions/sql-database-projects/src/test/project.test.ts @@ -10,7 +10,7 @@ import * as testUtils from './testUtils'; import * as constants from '../common/constants'; import { promises as fs } from 'fs'; -import { Project, EntryType, TargetPlatform } from '../models/project'; +import { Project, EntryType, TargetPlatform, SystemDatabase, DatabaseReferenceLocation } from '../models/project'; import { exists } from '../common/utils'; import { Uri } from 'vscode'; @@ -34,6 +34,9 @@ describe('Project: sqlproj content operations', function (): void { should(project.files.find(f => f.type === EntryType.Folder && f.relativePath === 'Views\\User')).not.equal(undefined); // mixed ItemGroup folder should(project.files.find(f => f.type === EntryType.File && f.relativePath === 'Views\\User\\Profile.sql')).not.equal(undefined); // mixed ItemGroup file + + should(project.databaseReferences.length).equal(1); + should(project.databaseReferences[0]).containEql(constants.master); }); it('Should add Folder and Build entries to sqlproj', async function (): Promise { @@ -127,6 +130,25 @@ describe('Project: sqlproj content operations', function (): void { project.changeDSP('invalidPlatform'); await testUtils.shouldThrowSpecificError(async () => await project.getSystemDacpacUri(constants.masterDacpac), constants.invalidDataSchemaProvider); }); + + it('Should add database references correctly', async function(): Promise { + projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); + const project = new Project(projFilePath); + await project.readProjFile(); + + should(project.databaseReferences.length).equal(0); + await project.addSystemDatabaseReference(SystemDatabase.master); + should(project.databaseReferences.length).equal(1); + should(project.databaseReferences[0]).equal(constants.master); + + await project.addSystemDatabaseReference(SystemDatabase.msdb); + should(project.databaseReferences.length).equal(2); + should(project.databaseReferences[1]).equal(constants.msdb); + + await project.addDatabaseReference(Uri.parse('test.dacpac'), DatabaseReferenceLocation.sameDatabase, false); + should(project.databaseReferences.length).equal(3); + should(project.databaseReferences[2]).equal('test'); + }); }); describe('Project: round trip updates', function (): void { diff --git a/extensions/sql-database-projects/src/test/projectTree.test.ts b/extensions/sql-database-projects/src/test/projectTree.test.ts index 7101d9a429..1b45f26f1d 100644 --- a/extensions/sql-database-projects/src/test/projectTree.test.ts +++ b/extensions/sql-database-projects/src/test/projectTree.test.ts @@ -68,6 +68,7 @@ describe('Project Tree tests', function (): void { const tree = new ProjectRootTreeItem(proj); should(tree.children.map(x => x.uri.path)).deepEqual([ '/TestProj.sqlproj/Data Sources', + '/TestProj.sqlproj/Database References', '/TestProj.sqlproj/duplicateFolder', '/TestProj.sqlproj/someFolder', '/TestProj.sqlproj/duplicate.sql']);