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 addDatabaseReferenceDialogName = localize('addDatabaseReferencedialogName', "Add database reference");
export const addDatabaseReferenceOkButtonText = localize('addDatabaseReferenceOkButtonText', "Add reference"); export const addDatabaseReferenceOkButtonText = localize('addDatabaseReferenceOkButtonText', "Add reference");
export const referenceRadioButtonsGroupTitle = localize('referenceRadioButtonsGroupTitle', "Type"); export const referenceRadioButtonsGroupTitle = localize('referenceRadioButtonsGroupTitle', "Type");
export const projectRadioButtonTitle = localize('projectRadioButtonTitle', "Database project in folder");
export const systemDatabaseRadioButtonTitle = localize('systemDatabaseRadioButtonTitle', "System database"); export const systemDatabaseRadioButtonTitle = localize('systemDatabaseRadioButtonTitle', "System database");
export const dacpacText = localize('dacpacText', "Data-tier application (.dacpac)"); export const dacpacText = localize('dacpacText', "Data-tier application (.dacpac)");
export const dacpacPlaceholder = localize('dacpacPlaceholder', "Select .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 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 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 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 // 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 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 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 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 // Action types
export const deleteAction = localize('deleteAction', 'Delete'); export const deleteAction = localize('deleteAction', 'Delete');
@@ -217,6 +220,8 @@ export const PostDeploy = 'PostDeploy';
export const None = 'None'; export const None = 'None';
export const True = 'True'; export const True = 'True';
export const False = 'False'; export const False = 'False';
export const Private = 'Private';
export const ProjectGuid = 'ProjectGuid';
// SqlProj File targets // SqlProj File targets
export const NetCoreTargets = '$(NETCoreTargetsPath)\\Microsoft.Data.Tools.Schema.SqlTasks.targets'; export const NetCoreTargets = '$(NETCoreTargetsPath)\\Microsoft.Data.Tools.Schema.SqlTasks.targets';

View File

@@ -6,8 +6,9 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as os from 'os'; import * as os from 'os';
import * as constants from './constants'; import * as constants from './constants';
import { promises as fs } from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as glob from 'fast-glob';
import { promises as fs } from 'fs';
/** /**
* Consolidates on the error message string * Consolidates on the error message string
@@ -143,3 +144,16 @@ export function readSqlCmdVariables(xmlDoc: any): Record<string, string> {
return sqlCmdVariables; 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 templates from '../templates/templates';
import * as constants from '../common/constants'; import * as constants from '../common/constants';
import * as path from 'path'; import * as path from 'path';
import * as glob from 'fast-glob';
import * as newProjectTool from '../tools/newProjectTool'; import * as newProjectTool from '../tools/newProjectTool';
import { SqlDatabaseProjectTreeViewProvider } from './databaseProjectTreeViewProvider'; import { SqlDatabaseProjectTreeViewProvider } from './databaseProjectTreeViewProvider';
import { getErrorMessage } from '../common/utils'; import { getErrorMessage, getSqlProjectFilesInFolder } from '../common/utils';
import { ProjectsController } from './projectController'; import { ProjectsController } from './projectController';
import { BaseProjectTreeItem } from '../models/tree/baseTreeItem'; import { BaseProjectTreeItem } from '../models/tree/baseTreeItem';
import { NetCoreTool } from '../tools/netcoreTool'; import { NetCoreTool } from '../tools/netcoreTool';
@@ -113,10 +112,7 @@ export default class MainController implements vscode.Disposable {
} }
public async loadProjectsInFolder(folderPath: string): Promise<void> { public async loadProjectsInFolder(folderPath: string): Promise<void> {
// path needs to use forward slashes for glob to work const results = await getSqlProjectFilesInFolder(folderPath);
let escapedPath = glob.escapePath(folderPath.replace(/\\/g, '/'));
let sqlprojFilter = path.posix.join(escapedPath, '**', '*.sqlproj');
let results = await glob(sqlprojFilter);
for (let f in results) { for (let f in results) {
// open the project, but don't switch focus to the file explorer viewlet // 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 { BuildHelper } from '../tools/buildHelper';
import { PublishProfile, load } from '../models/publishProfile/publishProfile'; import { PublishProfile, load } from '../models/publishProfile/publishProfile';
import { AddDatabaseReferenceDialog } from '../dialogs/addDatabaseReferenceDialog'; import { AddDatabaseReferenceDialog } from '../dialogs/addDatabaseReferenceDialog';
import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings } from '../models/IDatabaseReferenceSettings'; import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings, IProjectReferenceSettings } from '../models/IDatabaseReferenceSettings';
/** /**
* Controller for managing project lifecycle * Controller for managing project lifecycle
@@ -76,7 +76,6 @@ export class ProjectsController {
try { try {
await this.openProject(projUri, false, true); await this.openProject(projUri, false, true);
} catch (e) { } 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; return addDatabaseReferenceDialog;
} }
public async addDatabaseReferenceCallback(project: Project, settings: ISystemDatabaseReferenceSettings | IDacpacReferenceSettings): Promise<void> { public async addDatabaseReferenceCallback(project: Project, settings: ISystemDatabaseReferenceSettings | IDacpacReferenceSettings | IProjectReferenceSettings): Promise<void> {
try { 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); await project.addSystemDatabaseReference(<ISystemDatabaseReferenceSettings>settings);
} else { } else {
await project.addDatabaseReference(<IDacpacReferenceSettings>settings); await project.addDatabaseReference(<IDacpacReferenceSettings>settings);

View File

@@ -5,12 +5,14 @@
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as path from 'path';
import * as constants from '../common/constants'; 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 { cssStyles } from '../common/uiConstants';
import { IconPathHelper } from '../common/iconHelper'; 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 { export enum ReferenceType {
project, project,
@@ -28,6 +30,8 @@ export class AddDatabaseReferenceDialog {
public addDatabaseReferenceTab: azdata.window.DialogTab; public addDatabaseReferenceTab: azdata.window.DialogTab;
private view: azdata.ModelView | undefined; private view: azdata.ModelView | undefined;
private formBuilder: azdata.FormBuilder | undefined; private formBuilder: azdata.FormBuilder | undefined;
private projectDropdown: azdata.DropDownComponent | undefined;
private projectFormComponent: azdata.FormComponent | undefined;
private systemDatabaseDropdown: azdata.DropDownComponent | undefined; private systemDatabaseDropdown: azdata.DropDownComponent | undefined;
private systemDatabaseFormComponent: azdata.FormComponent | undefined; private systemDatabaseFormComponent: azdata.FormComponent | undefined;
public dacpacTextbox: azdata.InputBoxComponent | undefined; public dacpacTextbox: azdata.InputBoxComponent | undefined;
@@ -41,23 +45,16 @@ export class AddDatabaseReferenceDialog {
public exampleUsage: azdata.TextComponent | undefined; public exampleUsage: azdata.TextComponent | undefined;
public currentReferenceType: ReferenceType | undefined; public currentReferenceType: ReferenceType | undefined;
private referenceLocationMap: Map<string, DatabaseReferenceLocation>;
private toDispose: vscode.Disposable[] = []; private toDispose: vscode.Disposable[] = [];
private initDialogComplete: Deferred<void> | undefined; private initDialogComplete: Deferred<void> | undefined;
private initDialogPromise: Promise<void> = new Promise<void>((resolve, reject) => this.initDialogComplete = { resolve, reject }); 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) { constructor(private project: Project) {
this.dialog = azdata.window.createModelViewDialog(constants.addDatabaseReferenceDialogName); this.dialog = azdata.window.createModelViewDialog(constants.addDatabaseReferenceDialogName);
this.addDatabaseReferenceTab = azdata.window.createTab(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> { public async openDialog(): Promise<void> {
@@ -84,6 +81,7 @@ export class AddDatabaseReferenceDialog {
private initializeTab(): void { private initializeTab(): void {
this.addDatabaseReferenceTab.registerContent(async view => { this.addDatabaseReferenceTab.registerContent(async view => {
this.view = view; this.view = view;
this.projectFormComponent = await this.createProjectDropdown();
const radioButtonGroup = this.createRadioButtons(); const radioButtonGroup = this.createRadioButtons();
this.systemDatabaseFormComponent = this.createSystemDatabaseDropdown(); this.systemDatabaseFormComponent = this.createSystemDatabaseDropdown();
this.dacpacFormComponent = this.createDacpacTextbox(); this.dacpacFormComponent = this.createDacpacTextbox();
@@ -100,7 +98,7 @@ export class AddDatabaseReferenceDialog {
title: '', title: '',
components: [ components: [
radioButtonGroup, radioButtonGroup,
this.systemDatabaseFormComponent, this.currentReferenceType === ReferenceType.project ? this.projectFormComponent : this.systemDatabaseFormComponent,
locationDropdown, locationDropdown,
variableSection, variableSection,
exampleUsage, exampleUsage,
@@ -118,15 +116,27 @@ export class AddDatabaseReferenceDialog {
let formModel = this.formBuilder.component(); let formModel = this.formBuilder.component();
await view.initializeModel(formModel); await view.initializeModel(formModel);
this.updateEnabledInputBoxes();
this.initDialogComplete?.resolve(); this.initDialogComplete?.resolve();
}); });
} }
public async addReferenceClick(): Promise<void> { 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 = { referenceSettings = {
databaseName: <string>this.databaseNameTextbox?.value, databaseName: <string>this.databaseNameTextbox?.value,
systemDb: <string>this.systemDatabaseDropdown?.value === constants.master ? SystemDatabase.master : SystemDatabase.msdb, systemDb: <string>this.systemDatabaseDropdown?.value === constants.master ? SystemDatabase.master : SystemDatabase.msdb,
@@ -135,14 +145,12 @@ export class AddDatabaseReferenceDialog {
} else { // this.currentReferenceType === ReferenceType.dacpac } else { // this.currentReferenceType === ReferenceType.dacpac
referenceSettings = { referenceSettings = {
databaseName: <string>this.databaseNameTextbox?.value, databaseName: <string>this.databaseNameTextbox?.value,
databaseLocation: <DatabaseReferenceLocation>this.referenceLocationMap.get(<string>this.locationDropdown?.value),
dacpacFileLocation: vscode.Uri.file(<string>this.dacpacTextbox?.value), dacpacFileLocation: vscode.Uri.file(<string>this.dacpacTextbox?.value),
databaseVariable: <string>this.databaseVariableTextbox?.value, databaseVariable: <string>this.databaseVariableTextbox?.value,
serverName: <string>this.serverNameTextbox?.value, serverName: <string>this.serverNameTextbox?.value,
serverVariable: <string>this.serverVariableTextbox?.value, serverVariable: <string>this.serverVariableTextbox?.value,
suppressMissingDependenciesErrors: <boolean>this.suppressMissingDependenciesErrorsCheckbox?.checked suppressMissingDependenciesErrors: <boolean>this.suppressMissingDependenciesErrorsCheckbox?.checked
}; };
// TODO: add project reference support
} }
await this.addReference!(this.project, referenceSettings); await this.addReference!(this.project, referenceSettings);
@@ -151,14 +159,22 @@ export class AddDatabaseReferenceDialog {
} }
private createRadioButtons(): azdata.FormComponent { 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() const systemDatabaseRadioButton = this.view!.modelBuilder.radioButton()
.withProperties({ .withProperties({
name: 'referenceType', name: 'referenceType',
label: constants.systemDatabaseRadioButtonTitle label: constants.systemDatabaseRadioButtonTitle
}).component(); }).component();
systemDatabaseRadioButton.checked = true;
systemDatabaseRadioButton.onDidClick(() => { systemDatabaseRadioButton.onDidClick(() => {
this.systemDbRadioButtonClick(); this.systemDbRadioButtonClick();
}); });
@@ -173,10 +189,20 @@ export class AddDatabaseReferenceDialog {
this.dacpacRadioButtonClick(); 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() let flexRadioButtonsModel: azdata.FlexContainer = this.view!.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' }) .withLayout({ flexFlow: 'column' })
.withItems([systemDatabaseRadioButton, dacpacRadioButton]) .withItems([projectRadioButton, systemDatabaseRadioButton, dacpacRadioButton])
.withProperties({ ariaRole: 'radiogroup' }) .withProperties({ ariaRole: 'radiogroup' })
.component(); .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 { public systemDbRadioButtonClick(): void {
this.formBuilder!.removeFormItem(<azdata.FormComponent>this.dacpacFormComponent); this.formBuilder!.removeFormItem(<azdata.FormComponent>this.dacpacFormComponent);
this.formBuilder!.removeFormItem(<azdata.FormComponent>this.projectFormComponent);
this.formBuilder!.insertFormItem(<azdata.FormComponent>this.systemDatabaseFormComponent, 2); 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 // update dropdown values because only different database, same server is a valid location for system db references
this.locationDropdown!.values = constants.systemDbLocationDropdownValues; this.locationDropdown!.values = constants.systemDbLocationDropdownValues;
this.locationDropdown!.value = constants.differentDbSameServer;
this.currentReferenceType = ReferenceType.systemDb; this.currentReferenceType = ReferenceType.systemDb;
this.updateEnabledInputBoxes(true); this.updateEnabledInputBoxes();
this.tryEnableAddReferenceButton(); this.tryEnableAddReferenceButton();
this.updateExampleUsage(); this.updateExampleUsage();
} }
public dacpacRadioButtonClick(): void { public dacpacRadioButtonClick(): void {
this.formBuilder!.removeFormItem(<azdata.FormComponent>this.systemDatabaseFormComponent); this.formBuilder!.removeFormItem(<azdata.FormComponent>this.systemDatabaseFormComponent);
this.formBuilder!.removeFormItem(<azdata.FormComponent>this.projectFormComponent);
this.formBuilder!.insertFormItem(<azdata.FormComponent>this.dacpacFormComponent, 2); this.formBuilder!.insertFormItem(<azdata.FormComponent>this.dacpacFormComponent, 2);
this.locationDropdown!.values = constants.locationDropdownValues; this.locationDropdown!.values = constants.locationDropdownValues;
@@ -212,6 +252,39 @@ export class AddDatabaseReferenceDialog {
this.updateExampleUsage(); 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 { private createSystemDatabaseDropdown(): azdata.FormComponent {
this.systemDatabaseDropdown = this.view!.modelBuilder.dropDown().withProperties({ this.systemDatabaseDropdown = this.view!.modelBuilder.dropDown().withProperties({
values: [constants.master, constants.msdb], values: [constants.master, constants.msdb],
@@ -286,9 +359,11 @@ export class AddDatabaseReferenceDialog {
private createLocationDropdown(): azdata.FormComponent { private createLocationDropdown(): azdata.FormComponent {
this.locationDropdown = this.view!.modelBuilder.dropDown().withProperties({ this.locationDropdown = this.view!.modelBuilder.dropDown().withProperties({
ariaLabel: constants.locationDropdown, ariaLabel: constants.locationDropdown,
values: constants.systemDbLocationDropdownValues values: this.currentReferenceType === ReferenceType.systemDb ? constants.systemDbLocationDropdownValues : constants.locationDropdownValues
}).component(); }).component();
this.locationDropdown.value = constants.differentDbSameServer;
this.locationDropdown.onValueChanged(() => { this.locationDropdown.onValueChanged(() => {
this.updateEnabledInputBoxes(); this.updateEnabledInputBoxes();
this.tryEnableAddReferenceButton(); 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 * Update the enabled input boxes based on what the location of the database reference selected in the dropdown is
* @param isSystemDb * @param isSystemDb
*/ */
public updateEnabledInputBoxes(isSystemDb: boolean = false): void { public updateEnabledInputBoxes(): void {
const isSystemDb = this.currentReferenceType === ReferenceType.systemDb;
if (this.locationDropdown?.value === constants.sameDatabase) { if (this.locationDropdown?.value === constants.sameDatabase) {
this.databaseNameTextbox!.enabled = false; this.databaseNameTextbox!.enabled = false;
this.databaseVariableTextbox!.enabled = false; this.databaseVariableTextbox!.enabled = false;
@@ -387,7 +464,7 @@ export class AddDatabaseReferenceDialog {
private createExampleUsage(): azdata.FormComponent { private createExampleUsage(): azdata.FormComponent {
this.exampleUsage = this.view!.modelBuilder.text().withProperties({ this.exampleUsage = this.view!.modelBuilder.text().withProperties({
value: constants.systemDatabaseReferenceRequired, value: this.currentReferenceType === ReferenceType.project ? constants.databaseNameRequiredVariableOptional : constants.systemDatabaseReferenceRequired,
CSSStyles: { 'user-select': 'text' } CSSStyles: { 'user-select': 'text' }
}).component(); }).component();
@@ -440,27 +517,35 @@ export class AddDatabaseReferenceDialog {
*/ */
public tryEnableAddReferenceButton(): void { public tryEnableAddReferenceButton(): void {
switch (this.currentReferenceType) { switch (this.currentReferenceType) {
case ReferenceType.project: {
this.dialog.okButton.enabled = this.projectRequiredFieldsFilled();
break;
}
case ReferenceType.systemDb: { case ReferenceType.systemDb: {
this.dialog.okButton.enabled = !!this.databaseNameTextbox?.value; this.dialog.okButton.enabled = !!this.databaseNameTextbox?.value;
break; break;
} }
case ReferenceType.dacpac: { case ReferenceType.dacpac: {
this.dialog.okButton.enabled = this.dacpacFieldsRequiredFieldsFilled(); this.dialog.okButton.enabled = this.dacpacRequiredFieldsFilled();
break; break;
} }
case ReferenceType.project: {
// TODO
}
} }
} }
private dacpacFieldsRequiredFieldsFilled(): boolean { private dacpacRequiredFieldsFilled(): boolean {
return !!this.dacpacTextbox?.value && return !!this.dacpacTextbox?.value &&
((this.locationDropdown?.value === constants.sameDatabase) ((this.locationDropdown?.value === constants.sameDatabase)
|| (this.locationDropdown?.value === constants.differentDbSameServer && this.differentDatabaseSameServerRequiredFieldsFilled()) || (this.locationDropdown?.value === constants.differentDbSameServer && this.differentDatabaseSameServerRequiredFieldsFilled())
|| ((this.locationDropdown?.value === constants.differentDbDifferentServer && this.differentDatabaseDifferentServerRequiredFieldsFilled()))); || ((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 { private differentDatabaseSameServerRequiredFieldsFilled(): boolean {
return !!this.databaseNameTextbox?.value; return !!this.databaseNameTextbox?.value;
} }

View File

@@ -518,7 +518,7 @@ export class PublishDatabaseDialog {
canSelectFiles: true, canSelectFiles: true,
canSelectFolders: false, canSelectFolders: false,
canSelectMany: false, canSelectMany: false,
defaultUri: vscode.workspace.workspaceFolders ? (vscode.workspace.workspaceFolders as vscode.WorkspaceFolder[])[0].uri : undefined, defaultUri: vscode.Uri.file(this.project.projectFolderPath),
filters: { filters: {
[constants.publishSettingsFiles]: ['publish.xml'] [constants.publishSettingsFiles]: ['publish.xml']
} }
@@ -533,11 +533,14 @@ export class PublishDatabaseDialog {
const result = await this.readPublishProfile(fileUris[0]); 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 // 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).values = [];
(<azdata.DropDownComponent>this.targetDatabaseDropDown).value = result.databaseName;
this.connectionId = result.connectionId; this.connectionId = result.connectionId;
await this.updateConnectionComponents(result.connection, <string>this.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) { for (let key in result.sqlCmdVariables) {
(<Record<string, string>>this.sqlCmdVars)[key] = result.sqlCmdVariables[key]; (<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. * 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'; import { Uri } from 'vscode';
export interface IDatabaseReferenceSettings { export interface IDatabaseReferenceSettings {
@@ -16,9 +16,17 @@ export interface ISystemDatabaseReferenceSettings extends IDatabaseReferenceSett
} }
export interface IDacpacReferenceSettings extends IDatabaseReferenceSettings { export interface IDacpacReferenceSettings extends IDatabaseReferenceSettings {
databaseLocation: DatabaseReferenceLocation;
dacpacFileLocation: Uri; dacpacFileLocation: Uri;
databaseVariable?: string; databaseVariable?: string;
serverName?: string; serverName?: string;
serverVariable?: 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 { Uri, window } from 'vscode';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { DataSource } from './dataSources/dataSources'; 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 * Class representing a Project, and providing functions for operating on it
@@ -22,6 +22,7 @@ import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings } from './ID
export class Project { export class Project {
public projectFilePath: string; public projectFilePath: string;
public projectFileName: string; public projectFileName: string;
public projectGuid: string | undefined;
public files: FileProjectEntry[] = []; public files: FileProjectEntry[] = [];
public dataSources: DataSource[] = []; public dataSources: DataSource[] = [];
public importedTargets: string[] = []; public importedTargets: string[] = [];
@@ -61,6 +62,9 @@ export class Project {
const projFileText = await fs.readFile(this.projectFilePath); const projFileText = await fs.readFile(this.projectFilePath);
this.projFileXmlDoc = new xmldom.DOMParser().parseFromString(projFileText.toString()); 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 // find all folders and files to include
for (let ig = 0; ig < this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ItemGroup).length; ig++) { for (let ig = 0; ig < this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ItemGroup).length; ig++) {
const itemGroup = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ItemGroup)[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 name = nameNodes.length === 1 ? nameNodes[0].childNodes[0].nodeValue : undefined;
const suppressMissingDependenciesErrorNode = references[r].getElementsByTagName(constants.SuppressMissingDependenciesErrors); 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({ this.databaseReferences.push(new DacpacReferenceProjectEntry({
dacpacFileLocation: Uri.file(utils.getPlatformSafeFileEntryPath(filepath)), dacpacFileLocation: Uri.file(utils.getPlatformSafeFileEntryPath(filepath)),
databaseLocation: name ? DatabaseReferenceLocation.differentDatabaseSameServer : DatabaseReferenceLocation.sameDatabase,
databaseName: name, databaseName: name,
suppressMissingDependenciesErrors: suppressMissingDependences suppressMissingDependenciesErrors: suppressMissingDependencies
})); }));
} }
} }
@@ -151,9 +154,14 @@ export class Project {
const name = nameNodes[0].childNodes[0].nodeValue; const name = nameNodes[0].childNodes[0].nodeValue;
const suppressMissingDependenciesErrorNode = projectReferences[r].getElementsByTagName(constants.SuppressMissingDependenciesErrors); 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); 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 * Adds a SQLCMD variable to the project
* @param name name of the variable * @param name name of the variable
@@ -529,10 +547,15 @@ export class Project {
throw new Error(constants.databaseReferenceAlreadyExists); throw new Error(constants.databaseReferenceAlreadyExists);
} }
const isSystemDatabaseProjectEntry = (<SystemDatabaseReferenceProjectEntry>entry).ssdtUri; if (entry instanceof SystemDatabaseReferenceProjectEntry) {
if (isSystemDatabaseProjectEntry) {
this.addSystemDatabaseReferenceToProjFile(<SystemDatabaseReferenceProjectEntry>entry); 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 { } else {
const referenceNode = this.projFileXmlDoc.createElement(constants.ArtifactReference); const referenceNode = this.projFileXmlDoc.createElement(constants.ArtifactReference);
referenceNode.setAttribute(constants.Include, entry.pathForSqlProj()); 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 { public addSqlCmdVariableToProjFile(entry: SqlCmdVariableProjectEntry): void {
// Remove any entries with the same variable name. It'll be replaced with a new one // Remove any entries with the same variable name. It'll be replaced with a new one
this.removeFromProjFile(entry); this.removeFromProjFile(entry);
@@ -821,7 +864,6 @@ export interface IDatabaseReferenceProjectEntry extends FileProjectEntry {
} }
export class DacpacReferenceProjectEntry extends FileProjectEntry implements IDatabaseReferenceProjectEntry { export class DacpacReferenceProjectEntry extends FileProjectEntry implements IDatabaseReferenceProjectEntry {
databaseLocation: DatabaseReferenceLocation;
databaseVariableLiteralValue?: string; databaseVariableLiteralValue?: string;
databaseSqlCmdVariable?: string; databaseSqlCmdVariable?: string;
serverName?: string; serverName?: string;
@@ -830,7 +872,6 @@ export class DacpacReferenceProjectEntry extends FileProjectEntry implements IDa
constructor(settings: IDacpacReferenceSettings) { constructor(settings: IDacpacReferenceSettings) {
super(settings.dacpacFileLocation, '', EntryType.DatabaseReference); super(settings.dacpacFileLocation, '', EntryType.DatabaseReference);
this.databaseLocation = settings.databaseLocation;
this.databaseSqlCmdVariable = settings.databaseVariable; this.databaseSqlCmdVariable = settings.databaseVariable;
this.databaseVariableLiteralValue = settings.databaseName; this.databaseVariableLiteralValue = settings.databaseName;
this.serverName = settings.serverName; this.serverName = settings.serverName;
@@ -870,13 +911,33 @@ class SystemDatabaseReferenceProjectEntry extends FileProjectEntry implements ID
} }
export class SqlProjectReferenceProjectEntry extends FileProjectEntry implements IDatabaseReferenceProjectEntry { export class SqlProjectReferenceProjectEntry extends FileProjectEntry implements IDatabaseReferenceProjectEntry {
constructor(uri: Uri, public projectName: string, public suppressMissingDependenciesErrors: boolean) { projectName: string;
super(uri, '', EntryType.DatabaseReference); 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 { public get databaseName(): string {
return this.projectName; 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 { export class SqlCmdVariableProjectEntry extends ProjectEntry {

View File

@@ -52,6 +52,7 @@ describe('Add Database Reference Dialog', () => {
// change location to different database, different server // change location to different database, different server
dialog.locationDropdown!.value = constants.differentDbDifferentServer; dialog.locationDropdown!.value = constants.differentDbDifferentServer;
dialog.updateEnabledInputBoxes();
dialog.tryEnableAddReferenceButton(); dialog.tryEnableAddReferenceButton();
should(dialog.dialog.okButton.enabled).equal(false, 'Ok button should not be enabled because server fields are not filled'); 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 // change location to same database
dialog.locationDropdown!.value = constants.sameDatabase; dialog.locationDropdown!.value = constants.sameDatabase;
dialog.updateEnabledInputBoxes();
dialog.tryEnableAddReferenceButton(); 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'); 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 // change reference type back to system db
dialog.systemDbRadioButtonClick(); dialog.systemDbRadioButtonClick();
should(dialog.databaseNameTextbox?.value).equal('', `Database name textbox should be empty. Actual:${dialog.databaseNameTextbox?.value}`); 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); const dialog = new AddDatabaseReferenceDialog(project);
await dialog.openDialog(); 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); should(dialog.currentReferenceType).equal(ReferenceType.systemDb);
validateInputBoxEnabledStates(dialog, { databaseNameEnabled: true, databaseVariableEnabled: false, serverNameEnabled: false, serverVariabledEnabled: false}); 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.locationDropdown!.value = constants.sameDatabase;
dialog.updateEnabledInputBoxes(); dialog.updateEnabledInputBoxes();
validateInputBoxEnabledStates(dialog, { databaseNameEnabled: false, databaseVariableEnabled: false, serverNameEnabled: false, serverVariabledEnabled: false}); 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 { function validateInputBoxEnabledStates(dialog: AddDatabaseReferenceDialog, expectedStates: inputBoxExpectedStates): void {
should(dialog.databaseNameTextbox?.enabled).equal(expectedStates.databaseNameEnabled); 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); 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); 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); 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 * as constants from '../common/constants';
import { promises as fs } from 'fs'; 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 { exists, convertSlashesForSqlProj } from '../common/utils';
import { Uri, window } from 'vscode'; import { Uri, window } from 'vscode';
@@ -217,7 +217,7 @@ describe('Project: sqlproj content operations', function (): void {
// add database reference in the same database // add database reference in the same database
should(project.databaseReferences.length).equal(0, 'There should be no database references to start with'); 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.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].databaseName).equal('test1', 'The database reference should be test1');
should(project.databaseReferences[0].suppressMissingDependenciesErrors).equal(true, 'project.databaseReferences[0].suppressMissingDependenciesErrors should be true'); 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'); should(project.databaseReferences.length).equal(0, 'There should be no database references to start with');
await project.addDatabaseReference({ await project.addDatabaseReference({
dacpacFileLocation: Uri.file('test2.dacpac'), dacpacFileLocation: Uri.file('test2.dacpac'),
databaseLocation: DatabaseReferenceLocation.differentDatabaseSameServer,
databaseName: 'test2DbName', databaseName: 'test2DbName',
databaseVariable: 'test2Db', databaseVariable: 'test2Db',
suppressMissingDependenciesErrors: false 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'); should(project.databaseReferences.length).equal(0, 'There should be no database references to start with');
await project.addDatabaseReference({ await project.addDatabaseReference({
dacpacFileLocation: Uri.file('test3.dacpac'), dacpacFileLocation: Uri.file('test3.dacpac'),
databaseLocation: DatabaseReferenceLocation.differentDatabaseDifferentServer,
databaseName: 'test3DbName', databaseName: 'test3DbName',
databaseVariable: 'test3Db', databaseVariable: 'test3Db',
serverName: 'otherServerName', 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.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].databaseName).equal('test3', 'The database reference should be test3');
should(project.databaseReferences[0].suppressMissingDependenciesErrors).equal(false, 'project.databaseReferences[0].suppressMissingDependenciesErrors should be false'); 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(); let projFileText = (await fs.readFile(projFilePath)).toString();
should(projFileText).containEql('test3.dacpac'); should(projFileText).containEql('test3.dacpac');
should(projFileText).containEql('<DatabaseSqlCmdVariable>test3Db</DatabaseSqlCmdVariable>'); should(projFileText).containEql('<DatabaseSqlCmdVariable>test3Db</DatabaseSqlCmdVariable>');
@@ -276,6 +274,87 @@ describe('Project: sqlproj content operations', function (): void {
should(projFileText).containEql('<SqlCmdVariable Include="otherServer">'); 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> { it('Should not allow adding duplicate database references', async function (): Promise<void> {
projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline);
const project = await Project.openProject(projFilePath); 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); 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'); 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.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'); should(project.databaseReferences[1].databaseName).equal('test', 'project.databaseReferences[1].databaseName should be test');
// try to add reference to test.dacpac again // 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'); 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 opened = false;
let addDbReferenceDialog = TypeMoq.Mock.ofType(AddDatabaseReferenceDialog); 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); let projController = TypeMoq.Mock.ofType(ProjectsController);
projController.callBase = true; 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> { 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)); const projPath = path.dirname(await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline));
await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, projPath);
const proj = new Project(projPath); const proj = new Project(projPath);
const addDbRefHoller = 'hello from callback for addDatabaseReference()'; 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'); 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');
});
}); });
}); });