Add reference to another sql project (#12186)

* add projects to add database reference dialog

* able to add project references

* check for circular dependency

* only allow adding reference to project in the same workspace

* fix location dropdown when project reference is enabled

* add tests

* more tests

* cleanup

* fix flakey test

* addressing comments
This commit is contained in:
Kim Santiago
2020-09-10 17:44:39 -07:00
committed by GitHub
parent 7df132b307
commit 133ff73a43
11 changed files with 380 additions and 70 deletions

View File

@@ -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';

View File

@@ -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<string, string> {
return sqlCmdVariables;
}
/**
* Recursively gets all the sqlproj files at any depth in a folder
* @param folderPath
*/
export async function getSqlProjectFilesInFolder(folderPath: string): Promise<string[]> {
// 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;
}

View File

@@ -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<void> {
// 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

View File

@@ -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<void> {
public async addDatabaseReferenceCallback(project: Project, settings: ISystemDatabaseReferenceSettings | IDacpacReferenceSettings | IProjectReferenceSettings): Promise<void> {
try {
if ((<ISystemDatabaseReferenceSettings>settings).systemDb !== undefined) {
if ((<IProjectReferenceSettings>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 ((<SqlProjectReferenceProjectEntry>r).projectName === project.projectFileName) {
vscode.window.showErrorMessage(constants.cantAddCircularProjectReference(referencedProject?.projectFileName!));
return;
}
}
await project.addProjectReference(projectReferenceSettings);
} else if ((<ISystemDatabaseReferenceSettings>settings).systemDb !== undefined) {
await project.addSystemDatabaseReference(<ISystemDatabaseReferenceSettings>settings);
} else {
await project.addDatabaseReference(<IDacpacReferenceSettings>settings);

View File

@@ -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<string, DatabaseReferenceLocation>;
private toDispose: vscode.Disposable[] = [];
private initDialogComplete: Deferred<void> | undefined;
private initDialogPromise: Promise<void> = new Promise<void>((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<void> {
@@ -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<void> {
let referenceSettings: ISystemDatabaseReferenceSettings | IDacpacReferenceSettings;
let referenceSettings: ISystemDatabaseReferenceSettings | IDacpacReferenceSettings | IProjectReferenceSettings;
if (this.currentReferenceType === ReferenceType.systemDb) {
if (this.currentReferenceType === ReferenceType.project) {
referenceSettings = {
projectName: <string>this.projectDropdown?.value,
projectGuid: '',
projectRelativePath: undefined,
databaseName: <string>this.databaseNameTextbox?.value,
databaseVariable: <string>this.databaseVariableTextbox?.value,
serverName: <string>this.serverNameTextbox?.value,
serverVariable: <string>this.serverVariableTextbox?.value,
suppressMissingDependenciesErrors: <boolean>this.suppressMissingDependenciesErrorsCheckbox?.checked
};
} else if (this.currentReferenceType === ReferenceType.systemDb) {
referenceSettings = {
databaseName: <string>this.databaseNameTextbox?.value,
systemDb: <string>this.systemDatabaseDropdown?.value === constants.master ? SystemDatabase.master : SystemDatabase.msdb,
@@ -135,14 +145,12 @@ export class AddDatabaseReferenceDialog {
} else { // this.currentReferenceType === ReferenceType.dacpac
referenceSettings = {
databaseName: <string>this.databaseNameTextbox?.value,
databaseLocation: <DatabaseReferenceLocation>this.referenceLocationMap.get(<string>this.locationDropdown?.value),
dacpacFileLocation: vscode.Uri.file(<string>this.dacpacTextbox?.value),
databaseVariable: <string>this.databaseVariableTextbox?.value,
serverName: <string>this.serverNameTextbox?.value,
serverVariable: <string>this.serverVariableTextbox?.value,
suppressMissingDependenciesErrors: <boolean>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(<azdata.FormComponent>this.dacpacFormComponent);
this.formBuilder!.removeFormItem(<azdata.FormComponent>this.systemDatabaseFormComponent);
this.formBuilder!.insertFormItem(<azdata.FormComponent>this.projectFormComponent, 2);
this.currentReferenceType = ReferenceType.project;
this.updateEnabledInputBoxes();
this.tryEnableAddReferenceButton();
this.updateExampleUsage();
}
public systemDbRadioButtonClick(): void {
this.formBuilder!.removeFormItem(<azdata.FormComponent>this.dacpacFormComponent);
this.formBuilder!.removeFormItem(<azdata.FormComponent>this.projectFormComponent);
this.formBuilder!.insertFormItem(<azdata.FormComponent>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(<azdata.FormComponent>this.systemDatabaseFormComponent);
this.formBuilder!.removeFormItem(<azdata.FormComponent>this.projectFormComponent);
this.formBuilder!.insertFormItem(<azdata.FormComponent>this.dacpacFormComponent, 2);
this.locationDropdown!.values = constants.locationDropdownValues;
@@ -212,6 +252,39 @@ export class AddDatabaseReferenceDialog {
this.updateExampleUsage();
}
private async createProjectDropdown(): Promise<azdata.FormComponent> {
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;
}

View File

@@ -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
(<azdata.DropDownComponent>this.targetDatabaseDropDown).values = [];
(<azdata.DropDownComponent>this.targetDatabaseDropDown).value = result.databaseName;
this.connectionId = result.connectionId;
await this.updateConnectionComponents(result.connection, <string>this.connectionId);
if (result.databaseName) {
(<azdata.DropDownComponent>this.targetDatabaseDropDown).value = result.databaseName;
}
for (let key in result.sqlCmdVariables) {
(<Record<string, string>>this.sqlCmdVars)[key] = result.sqlCmdVariables[key];
}

View File

@@ -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;
}

View File

@@ -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<void> {
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 = (<SystemDatabaseReferenceProjectEntry>entry).ssdtUri;
if (isSystemDatabaseProjectEntry) {
if (entry instanceof SystemDatabaseReferenceProjectEntry) {
this.addSystemDatabaseReferenceToProjFile(<SystemDatabaseReferenceProjectEntry>entry);
} else if (entry instanceof SqlProjectReferenceProjectEntry) {
const referenceNode = this.projFileXmlDoc.createElement(constants.ProjectReference);
referenceNode.setAttribute(constants.Include, entry.pathForSqlProj());
this.addProjectReferenceChildren(referenceNode, <SqlProjectReferenceProjectEntry>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 {

View File

@@ -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}`);
}

View File

@@ -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('<DatabaseSqlCmdVariable>test3Db</DatabaseSqlCmdVariable>');
@@ -276,6 +274,87 @@ describe('Project: sqlproj content operations', function (): void {
should(projFileText).containEql('<SqlCmdVariable Include="otherServer">');
});
it('Should add a project reference to the same database correctly', async function (): Promise<void> {
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<void> {
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('<DatabaseSqlCmdVariable>testdb</DatabaseSqlCmdVariable>');
should(projFileText).containEql('<SqlCmdVariable Include="testdb">');
});
it('Should add a project reference to a different database in a different server correctly', async function (): Promise<void> {
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('<DatabaseSqlCmdVariable>testdb</DatabaseSqlCmdVariable>');
should(projFileText).containEql('<SqlCmdVariable Include="testdb">');
should(projFileText).containEql('<ServerSqlCmdVariable>otherServer</ServerSqlCmdVariable>');
should(projFileText).containEql('<SqlCmdVariable Include="otherServer">');
});
it('Should not allow adding duplicate database references', async function (): Promise<void> {
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');
});

View File

@@ -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<void> {
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<void> {
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');
});
});
});