diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index d60f6d221e..3e76de1ae0 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -99,6 +99,7 @@ export const defaultUser = localize('default', "default"); export const addDatabaseReferenceDialogName = localize('addDatabaseReferencedialogName', "Add database reference"); export const addDatabaseReferenceOkButtonText = localize('addDatabaseReferenceOkButtonText', "Add reference"); export const referenceRadioButtonsGroupTitle = localize('referenceRadioButtonsGroupTitle', "Type"); +export const projectRadioButtonTitle = localize('projectRadioButtonTitle', "Database project in folder"); export const systemDatabaseRadioButtonTitle = localize('systemDatabaseRadioButtonTitle', "System database"); export const dacpacText = localize('dacpacText', "Data-tier application (.dacpac)"); export const dacpacPlaceholder = localize('dacpacPlaceholder', "Select .dacpac"); @@ -118,6 +119,7 @@ export const exampleUsage = localize('exampleUsage', "Example Usage"); export const enterSystemDbName = localize('enterSystemDbName', "Enter a database name for this system database"); export const databaseNameRequiredVariableOptional = localize('databaseNameRequiredVariableOptional', "A database name is required. The database variable is optional."); export const databaseNameServerNameVariableRequired = localize('databaseNameServerNameVariableRequired', "A database name, server name, and server variable are required. The database variable is optional"); +export const databaseProject = localize('databaseProject', "Database project"); // Error messages @@ -164,6 +166,7 @@ export function unexpectedProjectContext(uri: string) { return localize('unexpec export function unableToPerformAction(action: string, uri: string) { return localize('unableToPerformAction', "Unable to locate '{0}' target: '{1}'", action, uri); } export function unableToFindObject(path: string, objType: string) { return localize('unableToFindFile', "Unable to find {1} with path '{0}'", path, objType); } export function deployScriptExists(scriptType: string) { return localize('deployScriptExists', "A {0} script already exists. The new script will not be included in build.", scriptType); } +export function cantAddCircularProjectReference(project: string) { return localize('cantAddCircularProjectReference', "A reference to project '{0} cannot be added. Adding this project as a reference would cause a circular dependency", project); } // Action types export const deleteAction = localize('deleteAction', 'Delete'); @@ -217,6 +220,8 @@ export const PostDeploy = 'PostDeploy'; export const None = 'None'; export const True = 'True'; export const False = 'False'; +export const Private = 'Private'; +export const ProjectGuid = 'ProjectGuid'; // SqlProj File targets export const NetCoreTargets = '$(NETCoreTargetsPath)\\Microsoft.Data.Tools.Schema.SqlTasks.targets'; diff --git a/extensions/sql-database-projects/src/common/utils.ts b/extensions/sql-database-projects/src/common/utils.ts index b00c07dbae..8e0fb1cec1 100644 --- a/extensions/sql-database-projects/src/common/utils.ts +++ b/extensions/sql-database-projects/src/common/utils.ts @@ -6,8 +6,9 @@ import * as vscode from 'vscode'; import * as os from 'os'; import * as constants from './constants'; -import { promises as fs } from 'fs'; import * as path from 'path'; +import * as glob from 'fast-glob'; +import { promises as fs } from 'fs'; /** * Consolidates on the error message string @@ -143,3 +144,16 @@ export function readSqlCmdVariables(xmlDoc: any): Record { return sqlCmdVariables; } + +/** + * Recursively gets all the sqlproj files at any depth in a folder + * @param folderPath + */ +export async function getSqlProjectFilesInFolder(folderPath: string): Promise { + // path needs to use forward slashes for glob to work + const escapedPath = glob.escapePath(folderPath.replace(/\\/g, '/')); + const sqlprojFilter = path.posix.join(escapedPath, '**', '*.sqlproj'); + const results = await glob(sqlprojFilter); + + return results; +} diff --git a/extensions/sql-database-projects/src/controllers/mainController.ts b/extensions/sql-database-projects/src/controllers/mainController.ts index 7dc1fc470d..f8db219a49 100644 --- a/extensions/sql-database-projects/src/controllers/mainController.ts +++ b/extensions/sql-database-projects/src/controllers/mainController.ts @@ -9,11 +9,10 @@ import * as dataworkspace from 'dataworkspace'; import * as templates from '../templates/templates'; import * as constants from '../common/constants'; import * as path from 'path'; -import * as glob from 'fast-glob'; import * as newProjectTool from '../tools/newProjectTool'; import { SqlDatabaseProjectTreeViewProvider } from './databaseProjectTreeViewProvider'; -import { getErrorMessage } from '../common/utils'; +import { getErrorMessage, getSqlProjectFilesInFolder } from '../common/utils'; import { ProjectsController } from './projectController'; import { BaseProjectTreeItem } from '../models/tree/baseTreeItem'; import { NetCoreTool } from '../tools/netcoreTool'; @@ -113,10 +112,7 @@ export default class MainController implements vscode.Disposable { } public async loadProjectsInFolder(folderPath: string): Promise { - // path needs to use forward slashes for glob to work - let escapedPath = glob.escapePath(folderPath.replace(/\\/g, '/')); - let sqlprojFilter = path.posix.join(escapedPath, '**', '*.sqlproj'); - let results = await glob(sqlprojFilter); + const results = await getSqlProjectFilesInFolder(folderPath); for (let f in results) { // open the project, but don't switch focus to the file explorer viewlet diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index 988de17405..0a7ac5417c 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -28,7 +28,7 @@ import { NetCoreTool, DotNetCommandOptions } from '../tools/netcoreTool'; import { BuildHelper } from '../tools/buildHelper'; import { PublishProfile, load } from '../models/publishProfile/publishProfile'; import { AddDatabaseReferenceDialog } from '../dialogs/addDatabaseReferenceDialog'; -import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings } from '../models/IDatabaseReferenceSettings'; +import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings, IProjectReferenceSettings } from '../models/IDatabaseReferenceSettings'; /** * Controller for managing project lifecycle @@ -76,7 +76,6 @@ export class ProjectsController { try { await this.openProject(projUri, false, true); } catch (e) { - vscode.window.showErrorMessage(e.message === constants.projectAlreadyOpened(projUri.fsPath) ? constants.circularProjectReference(newProject.projectFileName, proj.databaseName) : e.message); } } @@ -486,9 +485,28 @@ export class ProjectsController { return addDatabaseReferenceDialog; } - public async addDatabaseReferenceCallback(project: Project, settings: ISystemDatabaseReferenceSettings | IDacpacReferenceSettings): Promise { + public async addDatabaseReferenceCallback(project: Project, settings: ISystemDatabaseReferenceSettings | IDacpacReferenceSettings | IProjectReferenceSettings): Promise { try { - if ((settings).systemDb !== undefined) { + if ((settings).projectName !== undefined) { + // get project path and guid + const projectReferenceSettings = settings as IProjectReferenceSettings; + const referencedProject = this.projects.find(p => p.projectFileName === projectReferenceSettings.projectName); + const relativePath = path.relative(project.projectFolderPath, referencedProject?.projectFilePath!); + projectReferenceSettings.projectRelativePath = vscode.Uri.file(relativePath); + projectReferenceSettings.projectGuid = referencedProject?.projectGuid!; + + const projectReferences = referencedProject?.databaseReferences.filter(r => r instanceof SqlProjectReferenceProjectEntry) ?? []; + + // check for cirular dependency + for (let r of projectReferences) { + if ((r).projectName === project.projectFileName) { + vscode.window.showErrorMessage(constants.cantAddCircularProjectReference(referencedProject?.projectFileName!)); + return; + } + } + + await project.addProjectReference(projectReferenceSettings); + } else if ((settings).systemDb !== undefined) { await project.addSystemDatabaseReference(settings); } else { await project.addDatabaseReference(settings); diff --git a/extensions/sql-database-projects/src/dialogs/addDatabaseReferenceDialog.ts b/extensions/sql-database-projects/src/dialogs/addDatabaseReferenceDialog.ts index 4f68b0af80..fb135c61d1 100644 --- a/extensions/sql-database-projects/src/dialogs/addDatabaseReferenceDialog.ts +++ b/extensions/sql-database-projects/src/dialogs/addDatabaseReferenceDialog.ts @@ -5,12 +5,14 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; +import * as path from 'path'; import * as constants from '../common/constants'; -import { Project, SystemDatabase, DatabaseReferenceLocation } from '../models/project'; +import { Project, SystemDatabase } from '../models/project'; import { cssStyles } from '../common/uiConstants'; import { IconPathHelper } from '../common/iconHelper'; -import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings } from '../models/IDatabaseReferenceSettings'; +import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings, IProjectReferenceSettings } from '../models/IDatabaseReferenceSettings'; +import { getSqlProjectFilesInFolder } from '../common/utils'; export enum ReferenceType { project, @@ -28,6 +30,8 @@ export class AddDatabaseReferenceDialog { public addDatabaseReferenceTab: azdata.window.DialogTab; private view: azdata.ModelView | undefined; private formBuilder: azdata.FormBuilder | undefined; + private projectDropdown: azdata.DropDownComponent | undefined; + private projectFormComponent: azdata.FormComponent | undefined; private systemDatabaseDropdown: azdata.DropDownComponent | undefined; private systemDatabaseFormComponent: azdata.FormComponent | undefined; public dacpacTextbox: azdata.InputBoxComponent | undefined; @@ -41,23 +45,16 @@ export class AddDatabaseReferenceDialog { public exampleUsage: azdata.TextComponent | undefined; public currentReferenceType: ReferenceType | undefined; - private referenceLocationMap: Map; private toDispose: vscode.Disposable[] = []; private initDialogComplete: Deferred | undefined; private initDialogPromise: Promise = new Promise((resolve, reject) => this.initDialogComplete = { resolve, reject }); - public addReference: ((proj: Project, settings: ISystemDatabaseReferenceSettings | IDacpacReferenceSettings) => any) | undefined; + public addReference: ((proj: Project, settings: ISystemDatabaseReferenceSettings | IDacpacReferenceSettings | IProjectReferenceSettings) => any) | undefined; constructor(private project: Project) { this.dialog = azdata.window.createModelViewDialog(constants.addDatabaseReferenceDialogName); this.addDatabaseReferenceTab = azdata.window.createTab(constants.addDatabaseReferenceDialogName); - - this.referenceLocationMap = new Map([ - [constants.sameDatabase, DatabaseReferenceLocation.sameDatabase], - [constants.differentDbSameServer, DatabaseReferenceLocation.differentDatabaseSameServer], - [constants.differentDbDifferentServer, DatabaseReferenceLocation.differentDatabaseDifferentServer] - ]); } public async openDialog(): Promise { @@ -84,6 +81,7 @@ export class AddDatabaseReferenceDialog { private initializeTab(): void { this.addDatabaseReferenceTab.registerContent(async view => { this.view = view; + this.projectFormComponent = await this.createProjectDropdown(); const radioButtonGroup = this.createRadioButtons(); this.systemDatabaseFormComponent = this.createSystemDatabaseDropdown(); this.dacpacFormComponent = this.createDacpacTextbox(); @@ -100,7 +98,7 @@ export class AddDatabaseReferenceDialog { title: '', components: [ radioButtonGroup, - this.systemDatabaseFormComponent, + this.currentReferenceType === ReferenceType.project ? this.projectFormComponent : this.systemDatabaseFormComponent, locationDropdown, variableSection, exampleUsage, @@ -118,15 +116,27 @@ export class AddDatabaseReferenceDialog { let formModel = this.formBuilder.component(); await view.initializeModel(formModel); + this.updateEnabledInputBoxes(); this.initDialogComplete?.resolve(); }); } public async addReferenceClick(): Promise { - let referenceSettings: ISystemDatabaseReferenceSettings | IDacpacReferenceSettings; + let referenceSettings: ISystemDatabaseReferenceSettings | IDacpacReferenceSettings | IProjectReferenceSettings; - if (this.currentReferenceType === ReferenceType.systemDb) { + if (this.currentReferenceType === ReferenceType.project) { + referenceSettings = { + projectName: this.projectDropdown?.value, + projectGuid: '', + projectRelativePath: undefined, + databaseName: this.databaseNameTextbox?.value, + databaseVariable: this.databaseVariableTextbox?.value, + serverName: this.serverNameTextbox?.value, + serverVariable: this.serverVariableTextbox?.value, + suppressMissingDependenciesErrors: this.suppressMissingDependenciesErrorsCheckbox?.checked + }; + } else if (this.currentReferenceType === ReferenceType.systemDb) { referenceSettings = { databaseName: this.databaseNameTextbox?.value, systemDb: this.systemDatabaseDropdown?.value === constants.master ? SystemDatabase.master : SystemDatabase.msdb, @@ -135,14 +145,12 @@ export class AddDatabaseReferenceDialog { } else { // this.currentReferenceType === ReferenceType.dacpac referenceSettings = { databaseName: this.databaseNameTextbox?.value, - databaseLocation: this.referenceLocationMap.get(this.locationDropdown?.value), dacpacFileLocation: vscode.Uri.file(this.dacpacTextbox?.value), databaseVariable: this.databaseVariableTextbox?.value, serverName: this.serverNameTextbox?.value, serverVariable: this.serverVariableTextbox?.value, suppressMissingDependenciesErrors: this.suppressMissingDependenciesErrorsCheckbox?.checked }; - // TODO: add project reference support } await this.addReference!(this.project, referenceSettings); @@ -151,14 +159,22 @@ export class AddDatabaseReferenceDialog { } private createRadioButtons(): azdata.FormComponent { - // TODO: add project reference button + const projectRadioButton = this.view!.modelBuilder.radioButton() + .withProperties({ + name: 'referenceType', + label: constants.projectRadioButtonTitle + }).component(); + + projectRadioButton.onDidClick(() => { + this.projectRadioButtonClick(); + }); + const systemDatabaseRadioButton = this.view!.modelBuilder.radioButton() .withProperties({ name: 'referenceType', label: constants.systemDatabaseRadioButtonTitle }).component(); - systemDatabaseRadioButton.checked = true; systemDatabaseRadioButton.onDidClick(() => { this.systemDbRadioButtonClick(); }); @@ -173,10 +189,20 @@ export class AddDatabaseReferenceDialog { this.dacpacRadioButtonClick(); }); - this.currentReferenceType = ReferenceType.systemDb; + if (this.projectDropdown?.values?.length) { + projectRadioButton.checked = true; + this.currentReferenceType = ReferenceType.project; + } else { + systemDatabaseRadioButton.checked = true; + this.currentReferenceType = ReferenceType.systemDb; + + // disable projects radio button if there aren't any projects that can be added as a reference + projectRadioButton.enabled = false; + } + let flexRadioButtonsModel: azdata.FlexContainer = this.view!.modelBuilder.flexContainer() .withLayout({ flexFlow: 'column' }) - .withItems([systemDatabaseRadioButton, dacpacRadioButton]) + .withItems([projectRadioButton, systemDatabaseRadioButton, dacpacRadioButton]) .withProperties({ ariaRole: 'radiogroup' }) .component(); @@ -186,21 +212,35 @@ export class AddDatabaseReferenceDialog { }; } + public projectRadioButtonClick(): void { + this.formBuilder!.removeFormItem(this.dacpacFormComponent); + this.formBuilder!.removeFormItem(this.systemDatabaseFormComponent); + this.formBuilder!.insertFormItem(this.projectFormComponent, 2); + + this.currentReferenceType = ReferenceType.project; + this.updateEnabledInputBoxes(); + this.tryEnableAddReferenceButton(); + this.updateExampleUsage(); + } + public systemDbRadioButtonClick(): void { this.formBuilder!.removeFormItem(this.dacpacFormComponent); + this.formBuilder!.removeFormItem(this.projectFormComponent); this.formBuilder!.insertFormItem(this.systemDatabaseFormComponent, 2); // update dropdown values because only different database, same server is a valid location for system db references this.locationDropdown!.values = constants.systemDbLocationDropdownValues; + this.locationDropdown!.value = constants.differentDbSameServer; this.currentReferenceType = ReferenceType.systemDb; - this.updateEnabledInputBoxes(true); + this.updateEnabledInputBoxes(); this.tryEnableAddReferenceButton(); this.updateExampleUsage(); } public dacpacRadioButtonClick(): void { this.formBuilder!.removeFormItem(this.systemDatabaseFormComponent); + this.formBuilder!.removeFormItem(this.projectFormComponent); this.formBuilder!.insertFormItem(this.dacpacFormComponent, 2); this.locationDropdown!.values = constants.locationDropdownValues; @@ -212,6 +252,39 @@ export class AddDatabaseReferenceDialog { this.updateExampleUsage(); } + private async createProjectDropdown(): Promise { + this.projectDropdown = this.view!.modelBuilder.dropDown().withProperties({ + ariaLabel: constants.databaseProject + }).component(); + + // get projects in workspace + const workspaceFolders = vscode.workspace.workspaceFolders; + if (workspaceFolders?.length) { + let projectFiles = await getSqlProjectFilesInFolder(workspaceFolders[0].uri.fsPath); + + // check if current project is in same open folder (should only be able to add a reference to another project in + // the folder if the current project is also in the folder) + if (projectFiles.find(p => p === this.project.projectFilePath)) { + // filter out current project + projectFiles = projectFiles.filter(p => p !== this.project.projectFilePath); + + projectFiles.forEach(p => { + projectFiles[projectFiles.indexOf(p)] = path.parse(p).name; + }); + + this.projectDropdown.values = projectFiles; + } else { + this.projectDropdown.values = []; + } + } + + return { + component: this.projectDropdown, + title: constants.databaseProject + }; + } + + private createSystemDatabaseDropdown(): azdata.FormComponent { this.systemDatabaseDropdown = this.view!.modelBuilder.dropDown().withProperties({ values: [constants.master, constants.msdb], @@ -286,9 +359,11 @@ export class AddDatabaseReferenceDialog { private createLocationDropdown(): azdata.FormComponent { this.locationDropdown = this.view!.modelBuilder.dropDown().withProperties({ ariaLabel: constants.locationDropdown, - values: constants.systemDbLocationDropdownValues + values: this.currentReferenceType === ReferenceType.systemDb ? constants.systemDbLocationDropdownValues : constants.locationDropdownValues }).component(); + this.locationDropdown.value = constants.differentDbSameServer; + this.locationDropdown.onValueChanged(() => { this.updateEnabledInputBoxes(); this.tryEnableAddReferenceButton(); @@ -305,7 +380,9 @@ export class AddDatabaseReferenceDialog { * Update the enabled input boxes based on what the location of the database reference selected in the dropdown is * @param isSystemDb */ - public updateEnabledInputBoxes(isSystemDb: boolean = false): void { + public updateEnabledInputBoxes(): void { + const isSystemDb = this.currentReferenceType === ReferenceType.systemDb; + if (this.locationDropdown?.value === constants.sameDatabase) { this.databaseNameTextbox!.enabled = false; this.databaseVariableTextbox!.enabled = false; @@ -387,7 +464,7 @@ export class AddDatabaseReferenceDialog { private createExampleUsage(): azdata.FormComponent { this.exampleUsage = this.view!.modelBuilder.text().withProperties({ - value: constants.systemDatabaseReferenceRequired, + value: this.currentReferenceType === ReferenceType.project ? constants.databaseNameRequiredVariableOptional : constants.systemDatabaseReferenceRequired, CSSStyles: { 'user-select': 'text' } }).component(); @@ -440,27 +517,35 @@ export class AddDatabaseReferenceDialog { */ public tryEnableAddReferenceButton(): void { switch (this.currentReferenceType) { + case ReferenceType.project: { + this.dialog.okButton.enabled = this.projectRequiredFieldsFilled(); + break; + } case ReferenceType.systemDb: { this.dialog.okButton.enabled = !!this.databaseNameTextbox?.value; break; } case ReferenceType.dacpac: { - this.dialog.okButton.enabled = this.dacpacFieldsRequiredFieldsFilled(); + this.dialog.okButton.enabled = this.dacpacRequiredFieldsFilled(); break; } - case ReferenceType.project: { - // TODO - } } } - private dacpacFieldsRequiredFieldsFilled(): boolean { + private dacpacRequiredFieldsFilled(): boolean { return !!this.dacpacTextbox?.value && ((this.locationDropdown?.value === constants.sameDatabase) || (this.locationDropdown?.value === constants.differentDbSameServer && this.differentDatabaseSameServerRequiredFieldsFilled()) || ((this.locationDropdown?.value === constants.differentDbDifferentServer && this.differentDatabaseDifferentServerRequiredFieldsFilled()))); } + private projectRequiredFieldsFilled(): boolean { + return !!this.projectDropdown?.value && + ((this.locationDropdown?.value === constants.sameDatabase) + || (this.locationDropdown?.value === constants.differentDbSameServer && this.differentDatabaseSameServerRequiredFieldsFilled()) + || ((this.locationDropdown?.value === constants.differentDbDifferentServer && this.differentDatabaseDifferentServerRequiredFieldsFilled()))); + } + private differentDatabaseSameServerRequiredFieldsFilled(): boolean { return !!this.databaseNameTextbox?.value; } diff --git a/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts b/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts index 5560a12756..ca9d49e0b5 100644 --- a/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts +++ b/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts @@ -518,7 +518,7 @@ export class PublishDatabaseDialog { canSelectFiles: true, canSelectFolders: false, canSelectMany: false, - defaultUri: vscode.workspace.workspaceFolders ? (vscode.workspace.workspaceFolders as vscode.WorkspaceFolder[])[0].uri : undefined, + defaultUri: vscode.Uri.file(this.project.projectFolderPath), filters: { [constants.publishSettingsFiles]: ['publish.xml'] } @@ -533,11 +533,14 @@ export class PublishDatabaseDialog { const result = await this.readPublishProfile(fileUris[0]); // clear out old database dropdown values. They'll get populated later if there was a connection specified in the profile (this.targetDatabaseDropDown).values = []; - (this.targetDatabaseDropDown).value = result.databaseName; this.connectionId = result.connectionId; await this.updateConnectionComponents(result.connection, this.connectionId); + if (result.databaseName) { + (this.targetDatabaseDropDown).value = result.databaseName; + } + for (let key in result.sqlCmdVariables) { (>this.sqlCmdVars)[key] = result.sqlCmdVariables[key]; } diff --git a/extensions/sql-database-projects/src/models/IDatabaseReferenceSettings.ts b/extensions/sql-database-projects/src/models/IDatabaseReferenceSettings.ts index 766d933306..f2207a8c37 100644 --- a/extensions/sql-database-projects/src/models/IDatabaseReferenceSettings.ts +++ b/extensions/sql-database-projects/src/models/IDatabaseReferenceSettings.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DatabaseReferenceLocation, SystemDatabase } from './project'; +import { SystemDatabase } from './project'; import { Uri } from 'vscode'; export interface IDatabaseReferenceSettings { @@ -16,9 +16,17 @@ export interface ISystemDatabaseReferenceSettings extends IDatabaseReferenceSett } export interface IDacpacReferenceSettings extends IDatabaseReferenceSettings { - databaseLocation: DatabaseReferenceLocation; dacpacFileLocation: Uri; databaseVariable?: string; serverName?: string; serverVariable?: string; } + +export interface IProjectReferenceSettings extends IDatabaseReferenceSettings { + projectRelativePath: Uri | undefined; + projectName: string; + projectGuid: string; + databaseVariable?: string; + serverName?: string; + serverVariable?: string; +} diff --git a/extensions/sql-database-projects/src/models/project.ts b/extensions/sql-database-projects/src/models/project.ts index e1ad09d81c..493e623424 100644 --- a/extensions/sql-database-projects/src/models/project.ts +++ b/extensions/sql-database-projects/src/models/project.ts @@ -14,7 +14,7 @@ import * as templates from '../templates/templates'; import { Uri, window } from 'vscode'; import { promises as fs } from 'fs'; import { DataSource } from './dataSources/dataSources'; -import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings } from './IDatabaseReferenceSettings'; +import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings, IProjectReferenceSettings } from './IDatabaseReferenceSettings'; /** * Class representing a Project, and providing functions for operating on it @@ -22,6 +22,7 @@ import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings } from './ID export class Project { public projectFilePath: string; public projectFileName: string; + public projectGuid: string | undefined; public files: FileProjectEntry[] = []; public dataSources: DataSource[] = []; public importedTargets: string[] = []; @@ -61,6 +62,9 @@ export class Project { const projFileText = await fs.readFile(this.projectFilePath); this.projFileXmlDoc = new xmldom.DOMParser().parseFromString(projFileText.toString()); + // get projectGUID + this.projectGuid = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ProjectGuid)[0].childNodes[0].nodeValue; + // 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]; @@ -128,13 +132,12 @@ export class Project { const name = nameNodes.length === 1 ? nameNodes[0].childNodes[0].nodeValue : undefined; const suppressMissingDependenciesErrorNode = references[r].getElementsByTagName(constants.SuppressMissingDependenciesErrors); - const suppressMissingDependences = suppressMissingDependenciesErrorNode[0].childNodes[0].nodeValue === true ?? false; + const suppressMissingDependencies = suppressMissingDependenciesErrorNode[0].childNodes[0].nodeValue === true ?? false; this.databaseReferences.push(new DacpacReferenceProjectEntry({ dacpacFileLocation: Uri.file(utils.getPlatformSafeFileEntryPath(filepath)), - databaseLocation: name ? DatabaseReferenceLocation.differentDatabaseSameServer : DatabaseReferenceLocation.sameDatabase, databaseName: name, - suppressMissingDependenciesErrors: suppressMissingDependences + suppressMissingDependenciesErrors: suppressMissingDependencies })); } } @@ -151,9 +154,14 @@ export class Project { const name = nameNodes[0].childNodes[0].nodeValue; const suppressMissingDependenciesErrorNode = projectReferences[r].getElementsByTagName(constants.SuppressMissingDependenciesErrors); - const suppressMissingDependences = suppressMissingDependenciesErrorNode[0].childNodes[0].nodeValue === true ?? false; + const suppressMissingDependencies = suppressMissingDependenciesErrorNode[0].childNodes[0].nodeValue === true ?? false; - this.databaseReferences.push(new SqlProjectReferenceProjectEntry(Uri.file(utils.getPlatformSafeFileEntryPath(filepath)), name, suppressMissingDependences)); + this.databaseReferences.push(new SqlProjectReferenceProjectEntry({ + projectRelativePath: Uri.file(utils.getPlatformSafeFileEntryPath(filepath)), + projectName: name, + projectGuid: '', // don't care when just reading project as a reference + suppressMissingDependenciesErrors: suppressMissingDependencies + })); } } @@ -374,6 +382,16 @@ export class Project { await this.addToProjFile(databaseReferenceEntry); } + /** + * Adds reference to a another project in the workspace + * @param uri Uri of the dacpac + * @param databaseName name of the database + */ + public async addProjectReference(settings: IProjectReferenceSettings): Promise { + const projectReferenceEntry = new SqlProjectReferenceProjectEntry(settings); + await this.addToProjFile(projectReferenceEntry); + } + /** * Adds a SQLCMD variable to the project * @param name name of the variable @@ -529,10 +547,15 @@ export class Project { throw new Error(constants.databaseReferenceAlreadyExists); } - const isSystemDatabaseProjectEntry = (entry).ssdtUri; - - if (isSystemDatabaseProjectEntry) { + if (entry instanceof SystemDatabaseReferenceProjectEntry) { this.addSystemDatabaseReferenceToProjFile(entry); + } else if (entry instanceof SqlProjectReferenceProjectEntry) { + const referenceNode = this.projFileXmlDoc.createElement(constants.ProjectReference); + referenceNode.setAttribute(constants.Include, entry.pathForSqlProj()); + this.addProjectReferenceChildren(referenceNode, entry); + this.addDatabaseReferenceChildren(referenceNode, entry); + this.findOrCreateItemGroup(constants.ProjectReference).appendChild(referenceNode); + this.databaseReferences.push(entry); } else { const referenceNode = this.projFileXmlDoc.createElement(constants.ArtifactReference); referenceNode.setAttribute(constants.Include, entry.pathForSqlProj()); @@ -579,6 +602,26 @@ export class Project { } } + private addProjectReferenceChildren(referenceNode: any, entry: SqlProjectReferenceProjectEntry): void { + // project name + const nameElement = this.projFileXmlDoc.createElement(constants.Name); + const nameTextNode = this.projFileXmlDoc.createTextNode(entry.projectName); + nameElement.appendChild(nameTextNode); + referenceNode.appendChild(nameElement); + + // add project guid + const projectElement = this.projFileXmlDoc.createElement(constants.Project); + const projectGuidTextNode = this.projFileXmlDoc.createTextNode(entry.projectGuid); + projectElement.appendChild(projectGuidTextNode); + referenceNode.appendChild(projectElement); + + // add Private (not sure what this is for) + const privateElement = this.projFileXmlDoc.createElement(constants.Private); + const privateTextNode = this.projFileXmlDoc.createTextNode(constants.True); + privateElement.appendChild(privateTextNode); + referenceNode.appendChild(privateElement); + } + public addSqlCmdVariableToProjFile(entry: SqlCmdVariableProjectEntry): void { // Remove any entries with the same variable name. It'll be replaced with a new one this.removeFromProjFile(entry); @@ -821,7 +864,6 @@ export interface IDatabaseReferenceProjectEntry extends FileProjectEntry { } export class DacpacReferenceProjectEntry extends FileProjectEntry implements IDatabaseReferenceProjectEntry { - databaseLocation: DatabaseReferenceLocation; databaseVariableLiteralValue?: string; databaseSqlCmdVariable?: string; serverName?: string; @@ -830,7 +872,6 @@ export class DacpacReferenceProjectEntry extends FileProjectEntry implements IDa constructor(settings: IDacpacReferenceSettings) { super(settings.dacpacFileLocation, '', EntryType.DatabaseReference); - this.databaseLocation = settings.databaseLocation; this.databaseSqlCmdVariable = settings.databaseVariable; this.databaseVariableLiteralValue = settings.databaseName; this.serverName = settings.serverName; @@ -870,13 +911,33 @@ class SystemDatabaseReferenceProjectEntry extends FileProjectEntry implements ID } export class SqlProjectReferenceProjectEntry extends FileProjectEntry implements IDatabaseReferenceProjectEntry { - constructor(uri: Uri, public projectName: string, public suppressMissingDependenciesErrors: boolean) { - super(uri, '', EntryType.DatabaseReference); + projectName: string; + projectGuid: string; + databaseVariableLiteralValue?: string; + databaseSqlCmdVariable?: string; + serverName?: string; + serverSqlCmdVariable?: string; + suppressMissingDependenciesErrors: boolean; + + constructor(settings: IProjectReferenceSettings) { + super(settings.projectRelativePath!, '', EntryType.DatabaseReference); + this.projectName = settings.projectName; + this.projectGuid = settings.projectGuid; + this.databaseSqlCmdVariable = settings.databaseVariable; + this.databaseVariableLiteralValue = settings.databaseName; + this.serverName = settings.serverName; + this.serverSqlCmdVariable = settings.serverVariable; + this.suppressMissingDependenciesErrors = settings.suppressMissingDependenciesErrors; } public get databaseName(): string { return this.projectName; } + + public pathForSqlProj(): string { + // need to remove the leading slash from path for build to work on Windows + return utils.convertSlashesForSqlProj(this.fsUri.path.substring(1)); + } } export class SqlCmdVariableProjectEntry extends ProjectEntry { diff --git a/extensions/sql-database-projects/src/test/dialogs/addDatabaseReferenceDialog.test.ts b/extensions/sql-database-projects/src/test/dialogs/addDatabaseReferenceDialog.test.ts index 415fb1faee..506b44fc2e 100644 --- a/extensions/sql-database-projects/src/test/dialogs/addDatabaseReferenceDialog.test.ts +++ b/extensions/sql-database-projects/src/test/dialogs/addDatabaseReferenceDialog.test.ts @@ -52,6 +52,7 @@ describe('Add Database Reference Dialog', () => { // change location to different database, different server dialog.locationDropdown!.value = constants.differentDbDifferentServer; + dialog.updateEnabledInputBoxes(); dialog.tryEnableAddReferenceButton(); should(dialog.dialog.okButton.enabled).equal(false, 'Ok button should not be enabled because server fields are not filled'); @@ -63,9 +64,14 @@ describe('Add Database Reference Dialog', () => { // change location to same database dialog.locationDropdown!.value = constants.sameDatabase; + dialog.updateEnabledInputBoxes(); dialog.tryEnableAddReferenceButton(); should(dialog.dialog.okButton.enabled).equal(true, 'Ok button should be enabled because only dacpac location is needed for a reference located on the same database'); + // switch to project + dialog.projectRadioButtonClick(); + should(dialog.dialog.okButton.enabled).equal(false, 'Ok button should not be enabled because there are no projects in the dropdown'); + // change reference type back to system db dialog.systemDbRadioButtonClick(); should(dialog.databaseNameTextbox?.value).equal('', `Database name textbox should be empty. Actual:${dialog.databaseNameTextbox?.value}`); @@ -77,7 +83,7 @@ describe('Add Database Reference Dialog', () => { const dialog = new AddDatabaseReferenceDialog(project); await dialog.openDialog(); - // dialog starts with system db + // dialog starts with system db because there aren't any other projects in the workspace should(dialog.currentReferenceType).equal(ReferenceType.systemDb); validateInputBoxEnabledStates(dialog, { databaseNameEnabled: true, databaseVariableEnabled: false, serverNameEnabled: false, serverVariabledEnabled: false}); @@ -96,6 +102,12 @@ describe('Add Database Reference Dialog', () => { dialog.locationDropdown!.value = constants.sameDatabase; dialog.updateEnabledInputBoxes(); validateInputBoxEnabledStates(dialog, { databaseNameEnabled: false, databaseVariableEnabled: false, serverNameEnabled: false, serverVariabledEnabled: false}); + + // change to project reference + dialog.projectRadioButtonClick(); + should(dialog.currentReferenceType).equal(ReferenceType.project); + should(dialog.locationDropdown!.value).equal(constants.sameDatabase); + validateInputBoxEnabledStates(dialog, { databaseNameEnabled: false, databaseVariableEnabled: false, serverNameEnabled: false, serverVariabledEnabled: false}); }); }); @@ -107,8 +119,8 @@ interface inputBoxExpectedStates { } function validateInputBoxEnabledStates(dialog: AddDatabaseReferenceDialog, expectedStates: inputBoxExpectedStates): void { - should(dialog.databaseNameTextbox?.enabled).equal(expectedStates.databaseNameEnabled); - should(dialog.databaseVariableTextbox?.enabled).equal(expectedStates.databaseVariableEnabled); - should(dialog.serverNameTextbox?.enabled).equal(expectedStates.serverNameEnabled); - should(dialog.serverVariableTextbox?.enabled).equal(expectedStates.serverVariabledEnabled); + should(dialog.databaseNameTextbox?.enabled).equal(expectedStates.databaseNameEnabled, `Database name text box should be ${expectedStates.databaseNameEnabled}. Actual: ${dialog.databaseNameTextbox?.enabled}`); + should(dialog.databaseVariableTextbox?.enabled).equal(expectedStates.databaseVariableEnabled, `Database variable text box should be ${expectedStates.databaseVariableEnabled}. Actual: ${dialog.databaseVariableTextbox?.enabled}`); + should(dialog.serverNameTextbox?.enabled).equal(expectedStates.serverNameEnabled, `Server name text box should be ${expectedStates.serverNameEnabled}. Actual: ${dialog.serverNameTextbox?.enabled}`); + should(dialog.serverVariableTextbox?.enabled).equal(expectedStates.serverVariabledEnabled, `Server variable text box should be ${expectedStates.serverVariabledEnabled}. Actual: ${dialog.serverVariableTextbox?.enabled}`); } diff --git a/extensions/sql-database-projects/src/test/project.test.ts b/extensions/sql-database-projects/src/test/project.test.ts index 20694bece7..e8c3c13c35 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, DatabaseReferenceLocation, DacpacReferenceProjectEntry, SqlProjectReferenceProjectEntry } from '../models/project'; +import { Project, EntryType, TargetPlatform, SystemDatabase, DacpacReferenceProjectEntry, SqlProjectReferenceProjectEntry } from '../models/project'; import { exists, convertSlashesForSqlProj } from '../common/utils'; import { Uri, window } from 'vscode'; @@ -217,7 +217,7 @@ describe('Project: sqlproj content operations', function (): void { // add database reference in the same database should(project.databaseReferences.length).equal(0, 'There should be no database references to start with'); - await project.addDatabaseReference({ dacpacFileLocation: Uri.file('test1.dacpac'), databaseLocation: DatabaseReferenceLocation.sameDatabase, suppressMissingDependenciesErrors: true }); + await project.addDatabaseReference({ dacpacFileLocation: Uri.file('test1.dacpac'), suppressMissingDependenciesErrors: true }); should(project.databaseReferences.length).equal(1, 'There should be a database reference after adding a reference to test1'); should(project.databaseReferences[0].databaseName).equal('test1', 'The database reference should be test1'); should(project.databaseReferences[0].suppressMissingDependenciesErrors).equal(true, 'project.databaseReferences[0].suppressMissingDependenciesErrors should be true'); @@ -234,7 +234,6 @@ describe('Project: sqlproj content operations', function (): void { should(project.databaseReferences.length).equal(0, 'There should be no database references to start with'); await project.addDatabaseReference({ dacpacFileLocation: Uri.file('test2.dacpac'), - databaseLocation: DatabaseReferenceLocation.differentDatabaseSameServer, databaseName: 'test2DbName', databaseVariable: 'test2Db', suppressMissingDependenciesErrors: false @@ -257,7 +256,6 @@ describe('Project: sqlproj content operations', function (): void { should(project.databaseReferences.length).equal(0, 'There should be no database references to start with'); await project.addDatabaseReference({ dacpacFileLocation: Uri.file('test3.dacpac'), - databaseLocation: DatabaseReferenceLocation.differentDatabaseDifferentServer, databaseName: 'test3DbName', databaseVariable: 'test3Db', serverName: 'otherServerName', @@ -267,7 +265,7 @@ describe('Project: sqlproj content operations', function (): void { should(project.databaseReferences.length).equal(1, 'There should be a database reference after adding a reference to test3'); should(project.databaseReferences[0].databaseName).equal('test3', 'The database reference should be test3'); should(project.databaseReferences[0].suppressMissingDependenciesErrors).equal(false, 'project.databaseReferences[0].suppressMissingDependenciesErrors should be false'); - // make sure reference to test2.dacpac and SQLCMD variables were added + // make sure reference to test3.dacpac and SQLCMD variables were added let projFileText = (await fs.readFile(projFilePath)).toString(); should(projFileText).containEql('test3.dacpac'); should(projFileText).containEql('test3Db'); @@ -276,6 +274,87 @@ describe('Project: sqlproj content operations', function (): void { should(projFileText).containEql(''); }); + it('Should add a project reference to the same database correctly', async function (): Promise { + projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); + const project = await Project.openProject(projFilePath); + + // add database reference to a different database on a different server + should(project.databaseReferences.length).equal(0, 'There should be no database references to start with'); + should(Object.keys(project.sqlCmdVariables).length).equal(0, `There should be no sqlcmd variables to start with. Actual: ${Object.keys(project.sqlCmdVariables).length}`); + await project.addProjectReference({ + projectName: 'project1', + projectGuid: '', + projectRelativePath: Uri.file(path.join('..','project1', 'project1.sqlproj')), + suppressMissingDependenciesErrors: false + }); + should(project.databaseReferences.length).equal(1, 'There should be a database reference after adding a reference to project1'); + should(project.databaseReferences[0].databaseName).equal('project1', 'The database reference should be project1'); + should(project.databaseReferences[0].suppressMissingDependenciesErrors).equal(false, 'project.databaseReferences[0].suppressMissingDependenciesErrors should be false'); + should(Object.keys(project.sqlCmdVariables).length).equal(0, `There should be no sqlcmd variables added. Actual: ${Object.keys(project.sqlCmdVariables).length}`); + + // make sure reference to project1 and SQLCMD variables were added + let projFileText = (await fs.readFile(projFilePath)).toString(); + should(projFileText).containEql('project1'); + }); + + it('Should add a project reference to a different database in the same server correctly', async function (): Promise { + projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); + const project = await Project.openProject(projFilePath); + + // add database reference to a different database on a different server + should(project.databaseReferences.length).equal(0, 'There should be no database references to start with'); + should(Object.keys(project.sqlCmdVariables).length).equal(0, 'There should be no sqlcmd variables to start with'); + await project.addProjectReference({ + projectName: 'project1', + projectGuid: '', + projectRelativePath: Uri.file(path.join('..','project1', 'project1.sqlproj')), + databaseName: 'testdbName', + databaseVariable: 'testdb', + suppressMissingDependenciesErrors: false + }); + should(project.databaseReferences.length).equal(1, 'There should be a database reference after adding a reference to project1'); + should(project.databaseReferences[0].databaseName).equal('project1', 'The database reference should be project1'); + should(project.databaseReferences[0].suppressMissingDependenciesErrors).equal(false, 'project.databaseReferences[0].suppressMissingDependenciesErrors should be false'); + should(Object.keys(project.sqlCmdVariables).length).equal(1, `There should be one new sqlcmd variable added. Actual: ${Object.keys(project.sqlCmdVariables).length}`); + + // make sure reference to project1 and SQLCMD variables were added + let projFileText = (await fs.readFile(projFilePath)).toString(); + should(projFileText).containEql('project1'); + should(projFileText).containEql('testdb'); + should(projFileText).containEql(''); + }); + + it('Should add a project reference to a different database in a different server correctly', async function (): Promise { + projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); + const project = await Project.openProject(projFilePath); + + // add database reference to a different database on a different server + should(project.databaseReferences.length).equal(0, 'There should be no database references to start with'); + should(Object.keys(project.sqlCmdVariables).length).equal(0, 'There should be no sqlcmd variables to start with'); + await project.addProjectReference({ + projectName: 'project1', + projectGuid: '', + projectRelativePath: Uri.file(path.join('..','project1', 'project1.sqlproj')), + databaseName: 'testdbName', + databaseVariable: 'testdb', + serverName: 'otherServerName', + serverVariable: 'otherServer', + suppressMissingDependenciesErrors: false + }); + should(project.databaseReferences.length).equal(1, 'There should be a database reference after adding a reference to project1'); + should(project.databaseReferences[0].databaseName).equal('project1', 'The database reference should be project1'); + should(project.databaseReferences[0].suppressMissingDependenciesErrors).equal(false, 'project.databaseReferences[0].suppressMissingDependenciesErrors should be false'); + should(Object.keys(project.sqlCmdVariables).length).equal(2, `There should be two new sqlcmd variables added. Actual: ${Object.keys(project.sqlCmdVariables).length}`); + + // make sure reference to project1 and SQLCMD variables were added + let projFileText = (await fs.readFile(projFilePath)).toString(); + should(projFileText).containEql('project1'); + should(projFileText).containEql('testdb'); + should(projFileText).containEql(''); + should(projFileText).containEql('otherServer'); + should(projFileText).containEql(''); + }); + it('Should not allow adding duplicate database references', async function (): Promise { projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); const project = await Project.openProject(projFilePath); @@ -289,12 +368,12 @@ describe('Project: sqlproj content operations', function (): void { await testUtils.shouldThrowSpecificError(async () => await project.addSystemDatabaseReference({ databaseName: 'master', systemDb: SystemDatabase.master, suppressMissingDependenciesErrors: false }), constants.databaseReferenceAlreadyExists); should(project.databaseReferences.length).equal(1, 'There should only be one database reference after trying to add a reference to master again'); - await project.addDatabaseReference({ dacpacFileLocation: Uri.file('test.dacpac'), databaseLocation: DatabaseReferenceLocation.sameDatabase, suppressMissingDependenciesErrors: false }); + await project.addDatabaseReference({ dacpacFileLocation: Uri.file('test.dacpac'), suppressMissingDependenciesErrors: false }); should(project.databaseReferences.length).equal(2, 'There should be two database references after adding a reference to test.dacpac'); should(project.databaseReferences[1].databaseName).equal('test', 'project.databaseReferences[1].databaseName should be test'); // try to add reference to test.dacpac again - await testUtils.shouldThrowSpecificError(async () => await project.addDatabaseReference({ dacpacFileLocation: Uri.file('test.dacpac'), databaseLocation: DatabaseReferenceLocation.sameDatabase, suppressMissingDependenciesErrors: false }), constants.databaseReferenceAlreadyExists); + await testUtils.shouldThrowSpecificError(async () => await project.addDatabaseReference({ dacpacFileLocation: Uri.file('test.dacpac'), suppressMissingDependenciesErrors: false }), constants.databaseReferenceAlreadyExists); should(project.databaseReferences.length).equal(2, 'There should be two database references after trying to add a reference to test.dacpac again'); }); diff --git a/extensions/sql-database-projects/src/test/projectController.test.ts b/extensions/sql-database-projects/src/test/projectController.test.ts index 7e373b808d..588674bdba 100644 --- a/extensions/sql-database-projects/src/test/projectController.test.ts +++ b/extensions/sql-database-projects/src/test/projectController.test.ts @@ -518,7 +518,7 @@ describe('ProjectsController', function (): void { let opened = false; let addDbReferenceDialog = TypeMoq.Mock.ofType(AddDatabaseReferenceDialog); - addDbReferenceDialog.setup(x => x.openDialog()).returns(() => { opened = true; return Promise.resolve(undefined) }); + addDbReferenceDialog.setup(x => x.openDialog()).returns(() => { opened = true; return Promise.resolve(undefined); }); let projController = TypeMoq.Mock.ofType(ProjectsController); projController.callBase = true; @@ -530,7 +530,6 @@ describe('ProjectsController', function (): void { it('Callbacks are hooked up and called from Add database reference dialog', async function (): Promise { const projPath = path.dirname(await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline)); - await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, projPath); const proj = new Project(projPath); const addDbRefHoller = 'hello from callback for addDatabaseReference()'; @@ -557,6 +556,36 @@ describe('ProjectsController', function (): void { should(holler).equal(addDbRefHoller, 'executionCallback() is supposed to have been setup and called for add database reference scenario'); }); + + it('Should not allow adding circular project references', async function (): Promise { + const showErrorMessageSpy = sinon.spy(vscode.window, 'showErrorMessage'); + + const projPath1 = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline); + const projPath2 = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); + const projController = new ProjectsController(new SqlDatabaseProjectTreeViewProvider()); + + const project1 = await projController.openProject(vscode.Uri.file(projPath1)); + const project2 = await projController.openProject(vscode.Uri.file(projPath2)); + + // add project reference from project1 to project2 + await projController.addDatabaseReferenceCallback(project1, { + projectGuid: '', + projectName: 'TestProject', + projectRelativePath: undefined, + suppressMissingDependenciesErrors: false + }); + should(showErrorMessageSpy.notCalled).be.true('showErrorMessage should not have been called'); + + // try to add circular reference + await projController.addDatabaseReferenceCallback(project2, { + projectGuid: '', + projectName: 'TestProjectName', + projectRelativePath: undefined, + suppressMissingDependenciesErrors: false + }); + should(showErrorMessageSpy.called).be.true('showErrorMessage should have been called'); + }); + }); });