mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-16 17:22:29 -05:00
Database projects deploy (#10417)
* Hooking up deployment of SQL projects to the project build functionality and database selection UI * Adding ADS-side plumbing for sqlcmdvars
This commit is contained in:
@@ -10,7 +10,8 @@ const localize = nls.loadMessageBundle();
|
||||
// Placeholder values
|
||||
export const dataSourcesFileName = 'datasources.json';
|
||||
export const sqlprojExtension = '.sqlproj';
|
||||
export const initialCatalogSetting = 'Initial Catalog';
|
||||
|
||||
// Commands
|
||||
export const schemaCompareExtensionId = 'microsoft.schema-compare';
|
||||
export const sqlDatabaseProjectExtensionId = 'microsoft.sql-database-projects';
|
||||
export const mssqlExtensionId = 'microsoft.mssql';
|
||||
@@ -59,6 +60,8 @@ export const buildDacpacNotFound = localize('buildDacpacNotFound', "Dacpac creat
|
||||
export function projectAlreadyOpened(path: string) { return localize('projectAlreadyOpened', "Project '{0}' is already opened.", path); }
|
||||
export function projectAlreadyExists(name: string, path: string) { return localize('projectAlreadyExists', "A project named {0} already exists in {1}.", name, path); }
|
||||
export function mssqlNotFound(mssqlConfigDir: string) { return localize('mssqlNotFound', "Could not get mssql extension's install location at {0}", mssqlConfigDir); }
|
||||
export function projBuildFailed(errorMessage: string) { return localize('projBuildFailed', "Build failed. Check output pane for more details. {0}", errorMessage); }
|
||||
export function unexpectedProjectContext(uri: string) { return localize('unexpectedProjectContext', "Unable to establish project context. Command invoked from unexpected location: {0}", uri); }
|
||||
|
||||
// Project script types
|
||||
|
||||
@@ -73,3 +76,10 @@ export const ItemGroup = 'ItemGroup';
|
||||
export const Build = 'Build';
|
||||
export const Folder = 'Folder';
|
||||
export const Include = 'Include';
|
||||
|
||||
// SQL connection string components
|
||||
export const initialCatalogSetting = 'Initial Catalog';
|
||||
export const dataSourceSetting = 'Data Source';
|
||||
export const integratedSecuritySetting = 'Integrated Security';
|
||||
export const userIdSetting = 'User ID';
|
||||
export const passwordSetting = 'Password';
|
||||
|
||||
@@ -50,7 +50,7 @@ export default class MainController implements Disposable {
|
||||
this.apiWrapper.registerCommand('sqlDatabaseProjects.properties', async (node: BaseProjectTreeItem) => { await this.apiWrapper.showErrorMessage(`Properties not yet implemented: ${node.uri.path}`); }); // TODO
|
||||
|
||||
this.apiWrapper.registerCommand('sqlDatabaseProjects.build', async (node: BaseProjectTreeItem) => { await this.projectsController.buildProject(node); });
|
||||
this.apiWrapper.registerCommand('sqlDatabaseProjects.deploy', async (node: BaseProjectTreeItem) => { await this.projectsController.deploy(node); });
|
||||
this.apiWrapper.registerCommand('sqlDatabaseProjects.deploy', async (node: BaseProjectTreeItem) => { await this.projectsController.deployProject(node); });
|
||||
this.apiWrapper.registerCommand('sqlDatabaseProjects.schemaCompare', async (node: BaseProjectTreeItem) => { await this.projectsController.schemaCompare(node); });
|
||||
this.apiWrapper.registerCommand('sqlDatabaseProjects.import', async (node: BaseProjectTreeItem) => { await this.projectsController.import(node); });
|
||||
|
||||
|
||||
@@ -3,22 +3,25 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as constants from '../common/constants';
|
||||
import * as dataSources from '../models/dataSources/dataSources';
|
||||
import * as mssql from '../../../mssql';
|
||||
import * as path from 'path';
|
||||
import * as utils from '../common/utils';
|
||||
import * as UUID from 'vscode-languageclient/lib/utils/uuid';
|
||||
import * as templates from '../templates/templates';
|
||||
|
||||
import { Uri, QuickPickItem } from 'vscode';
|
||||
import { TaskExecutionMode } from 'azdata';
|
||||
import { promises as fs } from 'fs';
|
||||
import { Uri, QuickPickItem, extensions, Extension } from 'vscode';
|
||||
import { ApiWrapper } from '../common/apiWrapper';
|
||||
import { DeployDatabaseDialog } from '../dialogs/deployDatabaseDialog';
|
||||
import { Project } from '../models/project';
|
||||
import { SqlDatabaseProjectTreeViewProvider } from './databaseProjectTreeViewProvider';
|
||||
import { promises as fs } from 'fs';
|
||||
import { FolderNode } from '../models/tree/fileFolderTreeItem';
|
||||
import { IDeploymentProfile, IGenerateScriptProfile } from '../models/IDeploymentProfile';
|
||||
import { BaseProjectTreeItem } from '../models/tree/baseTreeItem';
|
||||
import { ProjectRootTreeItem } from '../models/tree/projectTreeItem';
|
||||
import { FolderNode } from '../models/tree/fileFolderTreeItem';
|
||||
import { DeployDatabaseDialog } from '../dialogs/deployDatabaseDialog';
|
||||
import { NetCoreTool, DotNetCommandOptions } from '../tools/netcoreTool';
|
||||
import { BuildHelper } from '../tools/buildHelper';
|
||||
|
||||
@@ -114,28 +117,82 @@ export class ProjectsController {
|
||||
}
|
||||
|
||||
public closeProject(treeNode: BaseProjectTreeItem) {
|
||||
const project = this.getProjectContextFromTreeNode(treeNode);
|
||||
const project = ProjectsController.getProjectFromContext(treeNode);
|
||||
this.projects = this.projects.filter((e) => { return e !== project; });
|
||||
this.refreshProjectsTree();
|
||||
}
|
||||
|
||||
public async buildProject(treeNode: BaseProjectTreeItem): Promise<void> {
|
||||
/**
|
||||
* Builds a project, producing a dacpac
|
||||
* @param treeNode a treeItem in a project's hierarchy, to be used to obtain a Project
|
||||
* @returns path of the built dacpac
|
||||
*/
|
||||
public async buildProject(treeNode: BaseProjectTreeItem): Promise<string>;
|
||||
/**
|
||||
* Builds a project, producing a dacpac
|
||||
* @param project Project to be built
|
||||
* @returns path of the built dacpac
|
||||
*/
|
||||
public async buildProject(project: Project): Promise<string>;
|
||||
public async buildProject(context: Project | BaseProjectTreeItem): Promise<string | undefined> {
|
||||
const project: Project = ProjectsController.getProjectFromContext(context);
|
||||
|
||||
// Check mssql extension for project dlls (tracking issue #10273)
|
||||
await this.buildHelper.createBuildDirFolder();
|
||||
|
||||
const project = this.getProjectContextFromTreeNode(treeNode);
|
||||
const options: DotNetCommandOptions = {
|
||||
commandTitle: 'Build',
|
||||
workingDirectory: project.projectFolderPath,
|
||||
argument: this.buildHelper.constructBuildArguments(project.projectFilePath, this.buildHelper.extensionBuildDirPath)
|
||||
};
|
||||
await this.netCoreTool.runDotnetCommand(options);
|
||||
try {
|
||||
await this.netCoreTool.runDotnetCommand(options);
|
||||
|
||||
return path.join(project.projectFolderPath, 'bin', 'Debug', `${project.projectFileName}.dacpac`);
|
||||
}
|
||||
catch (err) {
|
||||
this.apiWrapper.showErrorMessage(constants.projBuildFailed(utils.getErrorMessage(err)));
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public deploy(treeNode: BaseProjectTreeItem): void {
|
||||
const project = this.getProjectContextFromTreeNode(treeNode);
|
||||
const deployDatabaseDialog = new DeployDatabaseDialog(this.apiWrapper, project);
|
||||
/**
|
||||
* Builds and deploys a project
|
||||
* @param treeNode a treeItem in a project's hierarchy, to be used to obtain a Project
|
||||
*/
|
||||
public async deployProject(treeNode: BaseProjectTreeItem): Promise<DeployDatabaseDialog>;
|
||||
/**
|
||||
* Builds and deploys a project
|
||||
* @param project Project to be built and deployed
|
||||
*/
|
||||
public async deployProject(project: Project): Promise<DeployDatabaseDialog>;
|
||||
public async deployProject(context: Project | BaseProjectTreeItem): Promise<DeployDatabaseDialog> {
|
||||
const project: Project = ProjectsController.getProjectFromContext(context);
|
||||
let deployDatabaseDialog = this.getDeployDialog(project);
|
||||
|
||||
deployDatabaseDialog.deploy = async (proj, prof) => await this.executionCallback(proj, prof);
|
||||
deployDatabaseDialog.generateScript = async (proj, prof) => await this.executionCallback(proj, prof);
|
||||
|
||||
deployDatabaseDialog.openDialog();
|
||||
|
||||
return deployDatabaseDialog;
|
||||
}
|
||||
|
||||
public async executionCallback(project: Project, profile: IDeploymentProfile | IGenerateScriptProfile): Promise<mssql.DacFxResult | undefined> {
|
||||
const dacpacPath = await this.buildProject(project);
|
||||
|
||||
if (!dacpacPath) {
|
||||
return undefined; // buildProject() handles displaying the error
|
||||
}
|
||||
|
||||
const dacFxService = await ProjectsController.getDaxFxService();
|
||||
|
||||
if (profile as IDeploymentProfile) {
|
||||
return await dacFxService.deployDacpac(dacpacPath, profile.databaseName, (<IDeploymentProfile>profile).upgradeExisting, profile.connectionUri, TaskExecutionMode.execute, profile.sqlCmdVariables);
|
||||
}
|
||||
else {
|
||||
return await dacFxService.generateDeployScript(dacpacPath, profile.databaseName, profile.connectionUri, TaskExecutionMode.execute, profile.sqlCmdVariables);
|
||||
}
|
||||
}
|
||||
|
||||
public async schemaCompare(treeNode: BaseProjectTreeItem): Promise<void> {
|
||||
@@ -145,7 +202,7 @@ export class ProjectsController {
|
||||
await this.buildProject(treeNode);
|
||||
|
||||
// start schema compare with the dacpac produced from build
|
||||
const project = this.getProjectContextFromTreeNode(treeNode);
|
||||
const project = ProjectsController.getProjectFromContext(treeNode);
|
||||
const dacpacPath = path.join(project.projectFolderPath, 'bin', 'Debug', `${project.projectFileName}.dacpac`);
|
||||
|
||||
// check that dacpac exists
|
||||
@@ -160,12 +217,12 @@ export class ProjectsController {
|
||||
}
|
||||
|
||||
public async import(treeNode: BaseProjectTreeItem) {
|
||||
const project = this.getProjectContextFromTreeNode(treeNode);
|
||||
const project = ProjectsController.getProjectFromContext(treeNode);
|
||||
await this.apiWrapper.showErrorMessage(`Import not yet implemented: ${project.projectFilePath}`); // TODO
|
||||
}
|
||||
|
||||
public async addFolderPrompt(treeNode: BaseProjectTreeItem) {
|
||||
const project = this.getProjectContextFromTreeNode(treeNode);
|
||||
const project = ProjectsController.getProjectFromContext(treeNode);
|
||||
const newFolderName = await this.promptForNewObjectName(new templates.ProjectScriptType(templates.folder, constants.folderFriendlyName, ''), project);
|
||||
|
||||
if (!newFolderName) {
|
||||
@@ -180,7 +237,7 @@ export class ProjectsController {
|
||||
}
|
||||
|
||||
public async addItemPromptFromNode(treeNode: BaseProjectTreeItem, itemTypeName?: string) {
|
||||
await this.addItemPrompt(this.getProjectContextFromTreeNode(treeNode), this.getRelativePath(treeNode), itemTypeName);
|
||||
await this.addItemPrompt(ProjectsController.getProjectFromContext(treeNode), this.getRelativePath(treeNode), itemTypeName);
|
||||
}
|
||||
|
||||
public async addItemPrompt(project: Project, relativePath: string, itemTypeName?: string) {
|
||||
@@ -223,6 +280,30 @@ export class ProjectsController {
|
||||
|
||||
//#region Helper methods
|
||||
|
||||
public getDeployDialog(project: Project): DeployDatabaseDialog {
|
||||
return new DeployDatabaseDialog(this.apiWrapper, project);
|
||||
}
|
||||
|
||||
private static getProjectFromContext(context: Project | BaseProjectTreeItem) {
|
||||
if (context instanceof Project) {
|
||||
return context;
|
||||
}
|
||||
|
||||
if (context.root instanceof ProjectRootTreeItem) {
|
||||
return (<ProjectRootTreeItem>context.root).project;
|
||||
}
|
||||
else {
|
||||
throw new Error(constants.unexpectedProjectContext(context.uri.path));
|
||||
}
|
||||
}
|
||||
|
||||
private static async getDaxFxService(): Promise<mssql.IDacFxService> {
|
||||
const ext: Extension<any> = extensions.getExtension(mssql.extension.name)!;
|
||||
|
||||
await ext.activate();
|
||||
return (ext.exports as mssql.IExtension).dacFx;
|
||||
}
|
||||
|
||||
private macroExpansion(template: string, macroDict: Record<string, string>): string {
|
||||
const macroIndicator = '@@';
|
||||
let output = template;
|
||||
@@ -239,20 +320,6 @@ export class ProjectsController {
|
||||
return output;
|
||||
}
|
||||
|
||||
private getProjectContextFromTreeNode(treeNode: BaseProjectTreeItem): Project {
|
||||
if (!treeNode) {
|
||||
// TODO: prompt for which (currently-open) project when invoked via command pallet
|
||||
throw new Error('TODO: prompt for which project when invoked via command pallet');
|
||||
}
|
||||
|
||||
if (treeNode.root instanceof ProjectRootTreeItem) {
|
||||
return (treeNode.root as ProjectRootTreeItem).project;
|
||||
}
|
||||
else {
|
||||
throw new Error('Unable to establish project context. Command invoked from unexpected location: ' + treeNode.uri.path);
|
||||
}
|
||||
}
|
||||
|
||||
private async promptForNewObjectName(itemType: templates.ProjectScriptType, _project: Project): Promise<string | undefined> {
|
||||
// TODO: ask project for suggested name that doesn't conflict
|
||||
const suggestedName = itemType.friendlyName.replace(new RegExp('\s', 'g'), '') + '1';
|
||||
|
||||
@@ -4,14 +4,16 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import * as constants from '../common/constants';
|
||||
|
||||
import { Project } from '../models/project';
|
||||
import { DataSource } from '../models/dataSources/dataSources';
|
||||
import { SqlConnectionDataSource } from '../models/dataSources/sqlConnectionStringSource';
|
||||
import { ApiWrapper } from '../common/apiWrapper';
|
||||
import { IDeploymentProfile, IGenerateScriptProfile } from '../models/IDeploymentProfile';
|
||||
|
||||
interface DataSourceDropdownValue extends azdata.CategoryValue {
|
||||
dataSource: DataSource;
|
||||
dataSource: SqlConnectionDataSource;
|
||||
database: string;
|
||||
}
|
||||
|
||||
@@ -30,6 +32,11 @@ export class DeployDatabaseDialog {
|
||||
private connection: azdata.connection.Connection | undefined;
|
||||
private connectionIsDataSource: boolean | undefined;
|
||||
|
||||
private toDispose: vscode.Disposable[] = [];
|
||||
|
||||
public deploy: ((proj: Project, profile: IDeploymentProfile) => any) | undefined;
|
||||
public generateScript: ((proj: Project, profile: IGenerateScriptProfile) => any) | undefined;
|
||||
|
||||
constructor(private apiWrapper: ApiWrapper, private project: Project) {
|
||||
this.dialog = azdata.window.createModelViewDialog(constants.deployDialogName);
|
||||
this.deployTab = azdata.window.createTab(constants.deployDialogName);
|
||||
@@ -39,12 +46,12 @@ export class DeployDatabaseDialog {
|
||||
this.initializeDialog();
|
||||
this.dialog.okButton.label = constants.deployDialogOkButtonText;
|
||||
this.dialog.okButton.enabled = false;
|
||||
this.dialog.okButton.onClick(async () => await this.deploy());
|
||||
this.toDispose.push(this.dialog.okButton.onClick(async () => await this.deployClick()));
|
||||
|
||||
this.dialog.cancelButton.label = constants.cancelButtonText;
|
||||
|
||||
let generateScriptButton: azdata.window.Button = azdata.window.createButton(constants.generateScriptButtonText);
|
||||
generateScriptButton.onClick(async () => await this.generateScript());
|
||||
this.toDispose.push(generateScriptButton.onClick(async () => await this.generateScriptClick()));
|
||||
generateScriptButton.enabled = false;
|
||||
|
||||
this.dialog.customButtons = [];
|
||||
@@ -53,6 +60,9 @@ export class DeployDatabaseDialog {
|
||||
azdata.window.openDialog(this.dialog);
|
||||
}
|
||||
|
||||
private dispose(): void {
|
||||
this.toDispose.forEach(disposable => disposable.dispose());
|
||||
}
|
||||
|
||||
private initializeDialog(): void {
|
||||
this.initializeDeployTab();
|
||||
@@ -104,17 +114,77 @@ export class DeployDatabaseDialog {
|
||||
});
|
||||
}
|
||||
|
||||
private async deploy(): Promise<void> {
|
||||
// TODO: hook up with build and deploy
|
||||
public async getConnectionUri(): Promise<string> {
|
||||
// if target connection is a data source, have to check if already connected or if connection dialog needs to be opened
|
||||
let connId: string;
|
||||
|
||||
if (this.dataSourcesRadioButton?.checked) {
|
||||
const dataSource = (this.dataSourcesDropDown!.value! as DataSourceDropdownValue).dataSource;
|
||||
|
||||
const connProfile: azdata.IConnectionProfile = {
|
||||
serverName: dataSource.server,
|
||||
databaseName: dataSource.database,
|
||||
connectionName: dataSource.name,
|
||||
userName: dataSource.username,
|
||||
password: dataSource.password,
|
||||
authenticationType: dataSource.integratedSecurity ? 'Integrated' : 'SqlAuth',
|
||||
savePassword: false,
|
||||
providerName: 'MSSQL',
|
||||
saveProfile: true,
|
||||
id: dataSource.name + '-dataSource',
|
||||
options: []
|
||||
};
|
||||
|
||||
if (dataSource.integratedSecurity) {
|
||||
connId = (await azdata.connection.connect(connProfile, false, false)).connectionId;
|
||||
}
|
||||
else {
|
||||
connId = (await azdata.connection.openConnectionDialog(undefined, connProfile)).connectionId;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!this.connection) {
|
||||
throw new Error('Connection not defined.');
|
||||
}
|
||||
|
||||
connId = this.connection?.connectionId;
|
||||
}
|
||||
|
||||
return await azdata.connection.getUriForConnection(connId);
|
||||
}
|
||||
|
||||
private async generateScript(): Promise<void> {
|
||||
// TODO: hook up with build and generate script
|
||||
// if target connection is a data source, have to check if already connected or if connection dialog needs to be opened
|
||||
public async deployClick(): Promise<void> {
|
||||
const profile: IDeploymentProfile = {
|
||||
databaseName: this.getTargetDatabaseName(),
|
||||
upgradeExisting: true,
|
||||
connectionUri: await this.getConnectionUri(),
|
||||
sqlCmdVariables: this.project.sqlCmdVariables
|
||||
};
|
||||
|
||||
await this.deploy!(this.project, profile);
|
||||
|
||||
this.dispose();
|
||||
azdata.window.closeDialog(this.dialog);
|
||||
}
|
||||
|
||||
public async generateScriptClick(): Promise<void> {
|
||||
const profile: IGenerateScriptProfile = {
|
||||
databaseName: this.getTargetDatabaseName(),
|
||||
connectionUri: await this.getConnectionUri()
|
||||
};
|
||||
|
||||
if (this.generateScript) {
|
||||
await this.generateScript!(this.project, profile);
|
||||
}
|
||||
|
||||
this.dispose();
|
||||
azdata.window.closeDialog(this.dialog);
|
||||
}
|
||||
|
||||
private getTargetDatabaseName(): string {
|
||||
return this.targetDatabaseTextBox?.value ?? '';
|
||||
}
|
||||
|
||||
public getDefaultDatabaseName(): string {
|
||||
return this.project.projectFileName;
|
||||
}
|
||||
@@ -193,13 +263,13 @@ export class DeployDatabaseDialog {
|
||||
private createDataSourcesDropdown(view: azdata.ModelView): azdata.FormComponent {
|
||||
let dataSourcesValues: DataSourceDropdownValue[] = [];
|
||||
|
||||
this.project.dataSources.forEach(dataSource => {
|
||||
const dbName: string = (dataSource as SqlConnectionDataSource).getSetting(constants.initialCatalogSetting);
|
||||
this.project.dataSources.filter(d => d instanceof SqlConnectionDataSource).forEach(dataSource => {
|
||||
const dbName: string = (dataSource as SqlConnectionDataSource).database;
|
||||
const displayName: string = `${dataSource.name}`;
|
||||
dataSourcesValues.push({
|
||||
displayName: displayName,
|
||||
name: dataSource.name,
|
||||
dataSource: dataSource,
|
||||
dataSource: dataSource as SqlConnectionDataSource,
|
||||
database: dbName
|
||||
});
|
||||
});
|
||||
@@ -221,7 +291,7 @@ export class DeployDatabaseDialog {
|
||||
}
|
||||
|
||||
private setDatabaseToSelectedDataSourceDatabase(): void {
|
||||
if ((<DataSourceDropdownValue>this.dataSourcesDropDown!.value).database) {
|
||||
if ((<DataSourceDropdownValue>this.dataSourcesDropDown!.value)?.database) {
|
||||
this.targetDatabaseTextBox!.value = (<DataSourceDropdownValue>this.dataSourcesDropDown!.value).database;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export interface IDeploymentProfile {
|
||||
databaseName: string;
|
||||
connectionUri: string;
|
||||
upgradeExisting: boolean;
|
||||
sqlCmdVariables?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface IGenerateScriptProfile {
|
||||
databaseName: string;
|
||||
connectionUri: string;
|
||||
sqlCmdVariables?: Record<string, string>;
|
||||
}
|
||||
@@ -25,6 +25,27 @@ export class SqlConnectionDataSource extends DataSource {
|
||||
return constants.sqlConnectionStringFriendly;
|
||||
}
|
||||
|
||||
public get server(): string {
|
||||
return this.getSetting(constants.dataSourceSetting);
|
||||
}
|
||||
|
||||
public get database(): string {
|
||||
return this.getSetting(constants.initialCatalogSetting);
|
||||
}
|
||||
|
||||
public get integratedSecurity(): boolean {
|
||||
return this.getSetting(constants.integratedSecuritySetting).toLowerCase() === 'true';
|
||||
}
|
||||
|
||||
public get username(): string {
|
||||
return this.getSetting(constants.userIdSetting);
|
||||
}
|
||||
|
||||
public get password(): string {
|
||||
// TODO: secure password storage; https://github.com/microsoft/azuredatastudio/issues/10561
|
||||
return this.getSetting(constants.passwordSetting);
|
||||
}
|
||||
|
||||
constructor(name: string, connectionString: string) {
|
||||
super(name);
|
||||
|
||||
@@ -38,12 +59,12 @@ export class SqlConnectionDataSource extends DataSource {
|
||||
throw new Error(constants.invalidSqlConnectionString);
|
||||
}
|
||||
|
||||
this.connectionStringComponents[split[0]] = split[1];
|
||||
this.connectionStringComponents[split[0].toLocaleLowerCase()] = split[1];
|
||||
}
|
||||
}
|
||||
|
||||
public getSetting(settingName: string): string {
|
||||
return this.connectionStringComponents[settingName];
|
||||
return this.connectionStringComponents[settingName.toLocaleLowerCase()];
|
||||
}
|
||||
|
||||
public static fromJson(json: DataSourceJson): SqlConnectionDataSource {
|
||||
|
||||
@@ -19,6 +19,7 @@ export class Project {
|
||||
public projectFileName: string;
|
||||
public files: ProjectEntry[] = [];
|
||||
public dataSources: DataSource[] = [];
|
||||
public sqlCmdVariables: Record<string, string> = {};
|
||||
|
||||
public get projectFolderPath() {
|
||||
return path.dirname(this.projectFilePath);
|
||||
|
||||
@@ -22,9 +22,9 @@ describe('Data Sources: DataSource operations', function (): void {
|
||||
|
||||
should(dataSourceList[0].name).equal('Test Data Source 1');
|
||||
should(dataSourceList[0].type).equal(sql.SqlConnectionDataSource.type);
|
||||
should((dataSourceList[0] as sql.SqlConnectionDataSource).getSetting('Initial Catalog')).equal('testDb');
|
||||
should((dataSourceList[0] as sql.SqlConnectionDataSource).database).equal('testDb');
|
||||
|
||||
should(dataSourceList[1].name).equal('My Other Data Source');
|
||||
should((dataSourceList[1] as sql.SqlConnectionDataSource).getSetting('Integrated Security')).equal('False');
|
||||
should((dataSourceList[1] as sql.SqlConnectionDataSource).integratedSecurity).equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('Project: sqlproj content operations', function (): void {
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
projFilePath = await testUtils.createTestSqlProj(baselines.openProjectFileBaseline);
|
||||
projFilePath = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline);
|
||||
});
|
||||
|
||||
it('Should read Project from sqlproj', async function (): Promise<void> {
|
||||
|
||||
@@ -17,6 +17,9 @@ import { ProjectsController } from '../controllers/projectController';
|
||||
import { promises as fs } from 'fs';
|
||||
import { createContext, TestContext } from './testContext';
|
||||
import { Project } from '../models/project';
|
||||
import { DeployDatabaseDialog } from '../dialogs/deployDatabaseDialog';
|
||||
import { ApiWrapper } from '../common/apiWrapper';
|
||||
import { IDeploymentProfile, IGenerateScriptProfile } from '../models/IDeploymentProfile';
|
||||
|
||||
let testContext: TestContext;
|
||||
|
||||
@@ -27,43 +30,99 @@ describe('ProjectsController: project controller operations', function (): void
|
||||
await baselines.loadBaselines();
|
||||
});
|
||||
|
||||
it('Should create new sqlproj file with correct values', async function (): Promise<void> {
|
||||
const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider());
|
||||
const projFileDir = path.join(os.tmpdir(), `TestProject_${new Date().getTime()}`);
|
||||
describe('Project file operations and prompting', function (): void {
|
||||
it('Should create new sqlproj file with correct values', async function (): Promise<void> {
|
||||
const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider());
|
||||
const projFileDir = path.join(os.tmpdir(), `TestProject_${new Date().getTime()}`);
|
||||
|
||||
const projFilePath = await projController.createNewProject('TestProjectName', vscode.Uri.file(projFileDir), 'BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575');
|
||||
const projFilePath = await projController.createNewProject('TestProjectName', vscode.Uri.file(projFileDir), 'BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575');
|
||||
|
||||
let projFileText = (await fs.readFile(projFilePath)).toString();
|
||||
let projFileText = (await fs.readFile(projFilePath)).toString();
|
||||
|
||||
should(projFileText).equal(baselines.newProjectFileBaseline);
|
||||
});
|
||||
should(projFileText).equal(baselines.newProjectFileBaseline);
|
||||
});
|
||||
|
||||
it('Should load Project and associated DataSources', async function (): Promise<void> {
|
||||
// setup test files
|
||||
const folderPath = await testUtils.generateTestFolderPath();
|
||||
const sqlProjPath = await testUtils.createTestSqlProj(baselines.openProjectFileBaseline, folderPath);
|
||||
await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath);
|
||||
|
||||
const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider());
|
||||
|
||||
const project = await projController.openProject(vscode.Uri.file(sqlProjPath));
|
||||
|
||||
should(project.files.length).equal(9); // detailed sqlproj tests in their own test file
|
||||
should(project.dataSources.length).equal(2); // detailed datasources tests in their own test file
|
||||
});
|
||||
|
||||
it('Should return silently when no object name provided', async function (): Promise<void> {
|
||||
for (const name of ['', ' ', undefined]) {
|
||||
testContext.apiWrapper.reset();
|
||||
testContext.apiWrapper.setup(x => x.showInputBox(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(name));
|
||||
testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns((s) => { console.log('we throwin'); throw new Error(s); });
|
||||
it('Should load Project and associated DataSources', async function (): Promise<void> {
|
||||
// setup test files
|
||||
const folderPath = await testUtils.generateTestFolderPath();
|
||||
const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline, folderPath);
|
||||
await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath);
|
||||
|
||||
const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider());
|
||||
const project = new Project('FakePath');
|
||||
|
||||
should(project.files.length).equal(0);
|
||||
await projController.addItemPrompt(new Project('FakePath'), '', templates.script);
|
||||
should(project.files.length).equal(0, 'Expected to return without throwing an exception or adding a file when an empty/undefined name is provided.');
|
||||
}
|
||||
const project = await projController.openProject(vscode.Uri.file(sqlProjPath));
|
||||
|
||||
should(project.files.length).equal(9); // detailed sqlproj tests in their own test file
|
||||
should(project.dataSources.length).equal(2); // detailed datasources tests in their own test file
|
||||
});
|
||||
|
||||
it('Should return silently when no SQL object name provided in prompts', async function (): Promise<void> {
|
||||
for (const name of ['', ' ', undefined]) {
|
||||
testContext.apiWrapper.reset();
|
||||
testContext.apiWrapper.setup(x => x.showInputBox(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(name));
|
||||
testContext.apiWrapper.setup(x => x.showErrorMessage(TypeMoq.It.isAny())).returns((s) => { throw new Error(s); });
|
||||
|
||||
const projController = new ProjectsController(testContext.apiWrapper.object, new SqlDatabaseProjectTreeViewProvider());
|
||||
const project = new Project('FakePath');
|
||||
|
||||
should(project.files.length).equal(0);
|
||||
await projController.addItemPrompt(new Project('FakePath'), '', templates.script);
|
||||
should(project.files.length).equal(0, 'Expected to return without throwing an exception or adding a file when an empty/undefined name is provided.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Deployment and deployment script generation', function (): void {
|
||||
it('Deploy dialog should open from ProjectController', async function (): Promise<void> {
|
||||
let opened = false;
|
||||
|
||||
let deployDialog = TypeMoq.Mock.ofType(DeployDatabaseDialog);
|
||||
deployDialog.setup(x => x.openDialog()).returns(() => { opened = true; });
|
||||
|
||||
let projController = TypeMoq.Mock.ofType(ProjectsController);
|
||||
projController.callBase = true;
|
||||
projController.setup(x => x.getDeployDialog(TypeMoq.It.isAny())).returns(() => deployDialog.object);
|
||||
|
||||
await projController.object.deployProject(new Project('FakePath'));
|
||||
should(opened).equal(true);
|
||||
});
|
||||
|
||||
it('Callbacks are hooked up and called from Deploy 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 deployHoller = 'hello from callback for deploy()';
|
||||
const generateHoller = 'hello from callback for generateScript()';
|
||||
|
||||
let holler = 'nothing';
|
||||
|
||||
let deployDialog = TypeMoq.Mock.ofType(DeployDatabaseDialog, undefined, undefined, new ApiWrapper(), proj);
|
||||
deployDialog.callBase = true;
|
||||
deployDialog.setup(x => x.getConnectionUri()).returns(async () => 'fake|connection|uri');
|
||||
|
||||
let projController = TypeMoq.Mock.ofType(ProjectsController);
|
||||
projController.callBase = true;
|
||||
projController.setup(x => x.getDeployDialog(TypeMoq.It.isAny())).returns(() => deployDialog.object);
|
||||
projController.setup(x => x.executionCallback(TypeMoq.It.isAny(), TypeMoq.It.is((_): _ is IDeploymentProfile => true))).returns(async () => {
|
||||
holler = deployHoller;
|
||||
return undefined;
|
||||
});
|
||||
|
||||
projController.setup(x => x.executionCallback(TypeMoq.It.isAny(), TypeMoq.It.is((_): _ is IGenerateScriptProfile => true))).returns(async () => {
|
||||
holler = generateHoller;
|
||||
return undefined;
|
||||
});
|
||||
|
||||
let dialog = await projController.object.deployProject(proj);
|
||||
await dialog.deployClick();
|
||||
|
||||
should(holler).equal(deployHoller, 'executionCallback() is supposed to have been setup and called for Deploy scenario');
|
||||
|
||||
dialog = await projController.object.deployProject(proj);
|
||||
await dialog.generateScriptClick();
|
||||
|
||||
should(holler).equal(generateHoller, 'executionCallback() is supposed to have been setup and called for GenerateScript scenario');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import * as constants from '../common/constants';
|
||||
import { promises as fs } from 'fs';
|
||||
import should = require('should');
|
||||
import { AssertionError } from 'assert';
|
||||
import { Project } from '../models/project';
|
||||
|
||||
export async function shouldThrowSpecificError(block: Function, expectedMessage: string, details?: string) {
|
||||
let succeeded = false;
|
||||
@@ -26,10 +27,14 @@ export async function shouldThrowSpecificError(block: Function, expectedMessage:
|
||||
}
|
||||
}
|
||||
|
||||
export async function createTestSqlProj(contents: string, folderPath?: string): Promise<string> {
|
||||
export async function createTestSqlProjFile(contents: string, folderPath?: string): Promise<string> {
|
||||
return await createTestFile(contents, 'TestProject.sqlproj', folderPath);
|
||||
}
|
||||
|
||||
export async function createTestProject(contents: string, folderPath?: string): Promise<Project> {
|
||||
return new Project(await createTestSqlProjFile(contents, folderPath));
|
||||
}
|
||||
|
||||
export async function createTestDataSources(contents: string, folderPath?: string): Promise<string> {
|
||||
return await createTestFile(contents, constants.dataSourcesFileName, folderPath);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user