diff --git a/extensions/mssql/src/contracts.ts b/extensions/mssql/src/contracts.ts index 9b60ec5fd0..9e2f1ebeb0 100644 --- a/extensions/mssql/src/contracts.ts +++ b/extensions/mssql/src/contracts.ts @@ -908,10 +908,6 @@ export interface AddSqlCmdVariableParams extends SqlProjectParams { * Default value of the SQLCMD variable */ defaultValue: string; - /** - * Value of the SQLCMD variable, with or without the $() - */ - value: string; } export interface DeleteSqlCmdVariableParams extends SqlProjectParams { diff --git a/extensions/mssql/src/mssql.d.ts b/extensions/mssql/src/mssql.d.ts index 7a51fcb08c..57558b15f9 100644 --- a/extensions/mssql/src/mssql.d.ts +++ b/extensions/mssql/src/mssql.d.ts @@ -490,9 +490,8 @@ declare module 'mssql' { * @param projectUri Absolute path of the project, including .sqlproj * @param name Name of the SQLCMD variable * @param defaultValue Default value of the SQLCMD variable - * @param value Value of the SQLCMD variable, with or without the $() */ - addSqlCmdVariable(projectUri: string, name: string, defaultValue: string, value: string): Promise; + addSqlCmdVariable(projectUri: string, name: string, defaultValue: string): Promise; /** * Delete a SQLCMD variable from a project @@ -506,9 +505,8 @@ declare module 'mssql' { * @param projectUri Absolute path of the project, including .sqlproj * @param name Name of the SQLCMD variable * @param defaultValue Default value of the SQLCMD variable - * @param value Value of the SQLCMD variable, with or without the $() */ - updateSqlCmdVariable(projectUri: string, name: string, defaultValue: string, value: string): Promise; + updateSqlCmdVariable(projectUri: string, name: string, defaultValue: string): Promise; /** * Add a SQL object script to a project @@ -570,7 +568,7 @@ declare module 'mssql' { getSqlCmdVariables(projectUri: string): Promise; /** - * getSqlObjectScripts + * Get all the SQL object scripts in a project * @param projectUri Absolute path of the project, including .sqlproj */ getSqlObjectScripts(projectUri: string): Promise; @@ -708,7 +706,7 @@ declare module 'mssql' { } interface UserDatabaseReference extends DatabaseReference { - databaseVariable: SqlCmdVariable; + databaseVariable?: SqlCmdVariable; serverVariable?: SqlCmdVariable; } @@ -726,13 +724,8 @@ declare module 'mssql' { } export const enum SystemDatabase { - master = 0, - msdb = 1 - } - - export const enum ProjectType { - sdkStyle = 0, - legacyStyle = 1 + Master = 0, + MSDB = 1 } export interface SqlCmdVariable { diff --git a/extensions/mssql/src/sqlProjects/sqlProjectsService.ts b/extensions/mssql/src/sqlProjects/sqlProjectsService.ts index 4b4c8e8bc1..e35ccea583 100644 --- a/extensions/mssql/src/sqlProjects/sqlProjectsService.ts +++ b/extensions/mssql/src/sqlProjects/sqlProjectsService.ts @@ -274,8 +274,8 @@ export class SqlProjectsService implements mssql.ISqlProjectsService { * @param defaultValue Default value of the SQLCMD variable * @param value Value of the SQLCMD variable, with or without the $() */ - public async addSqlCmdVariable(projectUri: string, name: string, defaultValue: string, value: string): Promise { - const params: contracts.AddSqlCmdVariableParams = { projectUri: projectUri, name: name, defaultValue: defaultValue, value: value }; + public async addSqlCmdVariable(projectUri: string, name: string, defaultValue: string): Promise { + const params: contracts.AddSqlCmdVariableParams = { projectUri: projectUri, name: name, defaultValue: defaultValue }; return await this.runWithErrorHandling(contracts.AddSqlCmdVariableRequest.type, params); } @@ -296,8 +296,8 @@ export class SqlProjectsService implements mssql.ISqlProjectsService { * @param defaultValue Default value of the SQLCMD variable * @param value Value of the SQLCMD variable, with or without the $() */ - public async updateSqlCmdVariable(projectUri: string, name: string, defaultValue: string, value: string): Promise { - const params: contracts.AddSqlCmdVariableParams = { projectUri: projectUri, name: name, defaultValue: defaultValue, value: value }; + public async updateSqlCmdVariable(projectUri: string, name: string, defaultValue: string): Promise { + const params: contracts.AddSqlCmdVariableParams = { projectUri: projectUri, name: name, defaultValue: defaultValue }; return await this.runWithErrorHandling(contracts.UpdateSqlCmdVariableRequest.type, params); } diff --git a/extensions/sql-database-projects/package.json b/extensions/sql-database-projects/package.json index 1bced10c82..45964749a7 100644 --- a/extensions/sql-database-projects/package.json +++ b/extensions/sql-database-projects/package.json @@ -188,11 +188,6 @@ "title": "%sqlDatabaseProjects.generateProjectFromOpenApiSpec%", "category": "%sqlDatabaseProjects.displayName%" }, - { - "command": "sqlDatabaseProjects.convertToSdkStyleProject", - "title": "%sqlDatabaseProjects.convertToSdkStyleProject%", - "category": "%sqlDatabaseProjects.displayName%" - }, { "command": "sqlDatabaseProjects.openInDesigner", "title": "%sqlDatabaseProjects.openInDesigner%", @@ -320,10 +315,6 @@ "command": "sqlDatabaseProjects.changeTargetPlatform", "when": "false" }, - { - "command": "sqlDatabaseProjects.convertToSdkStyleProject", - "when": "false" - }, { "command": "sqlDatabaseProjects.openInDesigner", "when": "false" @@ -428,7 +419,7 @@ }, { "command": "sqlDatabaseProjects.exclude", - "when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.folder || viewItem =~ /^databaseProject.itemType.file/", + "when": "view == dataworkspace.views.main && viewItem =~ /^databaseProject.itemType.file/", "group": "9_dbProjectsLast@1" }, { diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index a808825843..90b70f99f5 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -10,23 +10,26 @@ import * as utils from '../common/utils'; const localize = nls.loadMessageBundle(); -// Placeholder values +//#region file extensions export const dataSourcesFileName = 'datasources.json'; export const sqlprojExtension = '.sqlproj'; export const sqlFileExtension = '.sql'; export const publishProfileExtension = '.publish.xml'; export const openApiSpecFileExtensions = ['yaml', 'yml', 'json']; + +//#endregion + +//#region Placeholder values export const schemaCompareExtensionId = 'microsoft.schema-compare'; export const master = 'master'; -export const masterDacpac = 'master.dacpac'; export const msdb = 'msdb'; -export const msdbDacpac = 'msdb.dacpac'; export const MicrosoftDatatoolsSchemaSqlSql = 'Microsoft.Data.Tools.Schema.Sql.Sql'; export const databaseSchemaProvider = 'DatabaseSchemaProvider'; export const sqlProjectSdk = 'Microsoft.Build.Sql'; -export const sqlProjectSdkVersion = '0.1.7-preview'; -// Project Provider +//#endregion + +//#region Project Provider export const emptySqlDatabaseProjectTypeId = 'EmptySqlDbProj'; export const emptyProjectTypeDisplayName = localize('emptyProjectTypeDisplayName', "SQL Server Database"); export const emptyProjectTypeDescription = localize('emptyProjectTypeDescription', "Develop and publish schemas for SQL Server databases starting from an empty project"); @@ -43,7 +46,9 @@ export const emptyAzureDbSqlDatabaseProjectTypeId = 'EmptyAzureSqlDbProj'; export const emptyAzureDbProjectTypeDisplayName = localize('emptyAzureDbProjectTypeDisplayName', "Azure SQL Database"); export const emptyAzureDbProjectTypeDescription = localize('emptyAzureDbProjectTypeDescription', "Develop and publish schemas for Azure SQL Database starting from an empty project"); -// Dashboard +//#endregion + +//#region Dashboard export const addItemAction = localize('addItemAction', "Add Item"); export const schemaCompareAction = localize('schemaCompareAction', "Schema Compare"); export const buildAction = localize('buildAction', "Build"); @@ -70,14 +75,18 @@ export const msec = localize('msec', "msec"); export const at = localize('at', "at"); -// commands +//#endregion + +//#region commands export const revealFileInOsCommand = 'revealFileInOS'; export const schemaCompareStartCommand = 'schemaCompare.start'; export const schemaCompareRunComparisonCommand = 'schemaCompare.runComparison'; export const vscodeOpenCommand = 'vscode.open'; export const refreshDataWorkspaceCommand = 'dataworkspace.refresh'; -// UI Strings +//#endregion + +//#region UI Strings export const databaseReferencesNodeName = localize('databaseReferencesNodeName', "Database References"); export const sqlcmdVariablesNodeName = localize('sqlcmdVariablesNodeName', "SQLCMD Variables"); export const sqlConnectionStringFriendly = localize('sqlConnectionStringFriendly', "SQL connection string"); @@ -112,8 +121,11 @@ export function projectUpdatedToSdkStyle(projectName: string) { return localize( export function convertToSdkStyleConfirmation(projectName: string) { return localize('convertToSdkStyleConfirmation', "The project '{0}' will not be fully compatible with SSDT after conversion. A backup copy of the project file will be created in the project folder prior to conversion. More information is available at https://aka.ms/sqlprojsdk. Continue with converting to SDK-style project?", projectName); } export function updatedToSdkStyleError(projectName: string) { return localize('updatedToSdkStyleError', "Converting the project {0} to SDK-style was unsuccessful. Changes to the .sqlproj have been rolled back.", projectName); } export const enterNewName = localize('enterNewName', "Enter new name"); +//#endregion -// Publish dialog strings +export const reservedProjectFolders = ['Properties', 'SQLCMD Variables', 'Database References']; + +//#region Publish dialog strings export const publishDialogName = localize('publishDialogName', "Publish project"); export const publish = localize('publish', "Publish"); export const cancelButtonText = localize('cancelButtonText', "Cancel"); @@ -152,7 +164,9 @@ export const selectDatabase = localize('selectDatabase', "Select database"); export const done = localize('done', "Done"); export const nameMustNotBeEmpty = localize('nameMustNotBeEmpty', "Name must not be empty"); -// Publish Dialog options +//#endregion + +//#region Publish Dialog options export const AdvancedOptionsButton = localize('advancedOptionsButton', 'Advanced...'); export const AdvancedPublishOptions = localize('advancedPublishOptions', 'Advanced Publish Options'); export const PublishOptions = localize('publishOptions', 'Publish Options'); @@ -163,7 +177,9 @@ export const OptionName: string = localize('optionName', "Option Name"); export const OptionInclude: string = localize('include', "Include"); export function OptionNotFoundWarningMessage(label: string) { return localize('optionNotFoundWarningMessage', "label: {0} does not exist in the options value name lookup", label); } -// Deploy +//#endregion + +//#region Deploy export const SqlServerName = 'SQL server'; export const AzureSqlServerName = 'Azure SQL server'; export const SqlServerDockerImageName = 'Microsoft SQL Server'; @@ -206,7 +222,9 @@ export const dockerImageLabelPrefix = 'source=sqldbproject'; export const dockerImageNamePrefix = 'sqldbproject'; export const dockerImageDefaultTag = 'latest'; -// Publish to Container +//#endregion + +//#region Publish to Container export const eulaAgreementTemplate = localize({ key: 'eulaAgreementTemplate', comment: ['The placeholders are contents of the line and should not be translated.'] }, "I accept the {0}."); export function eulaAgreementText(name: string) { return localize({ key: 'eulaAgreementText', comment: ['The placeholders are contents of the line and should not be translated.'] }, "I accept the {0}.", name); } export const eulaAgreementTitle = localize('eulaAgreementTitle', "Microsoft SQL Server License Agreement"); @@ -262,8 +280,9 @@ export function retrySucceedMessage(name: string, result: string) { return local export function retryFailedMessage(name: string, result: string, error: string) { return localize('retryFailedMessage', "Operation '{0}' failed. Re-trying... Current Result: {1}. Error: '{2}'", name, result, error); } export function retryMessage(name: string, error: string) { return localize('retryMessage', "Operation '{0}' failed. Re-trying... Error: '{1}' ", name, error); } -// Add Database Reference dialog strings +//#endregion +//#region Add Database Reference dialog strings export const addDatabaseReferenceDialogName = localize('addDatabaseReferencedialogName', "Add database reference"); export const addDatabaseReferenceOkButtonText = localize('addDatabaseReferenceOkButtonText', "Add reference"); export const referenceRadioButtonsGroupTitle = localize('referenceRadioButtonsGroupTitle', "Type"); @@ -291,8 +310,13 @@ export const databaseProject = localize('databaseProject', "Database project"); export const dacpacMustBeOnSameDrive = localize('dacpacNotOnSameDrive', "Dacpac references need to be located on the same drive as the project file."); export const dacpacNotOnSameDrive = (projectLocation: string): string => { return localize('dacpacNotOnSameDrive', "Dacpac references need to be located on the same drive as the project file. The project file is located at {0}", projectLocation); }; export const referenceType = localize('referenceType', "Reference type"); +export const excludeFolderNotSupported = localize('excludeFolderNotSupported', "Excluding folders is not yet supported"); +export const unhandledDeleteType = (itemType: string): string => { return localize('unhandledDeleteType', "Unhandled item type during delete: '{0}", itemType); } +export const unhandledExcludeType = (itemType: string): string => { return localize('unhandledDeleteType', "Unhandled item type during exclude: '{0}", itemType); } -// Create Project From Database dialog strings +//#endregion + +//#region Create Project From Database dialog strings export const createProjectFromDatabaseDialogName = localize('createProjectFromDatabaseDialogName', "Create project from database"); export const createProjectDialogOkButtonText = localize('createProjectDialogOkButtonText', "Create"); export const sourceDatabase = localize('sourceDatabase', "Source database"); @@ -307,7 +331,6 @@ export const selectFolderStructure = localize('selectFolderStructure', "Select f export const folderStructureLabel = localize('folderStructureLabel', "Folder structure"); export const includePermissionsLabel = localize('includePermissionsLabel', "Include permissions"); export const includePermissionsInProject = localize('includePermissionsInProject', "Include permissions in project"); -export const WorkspaceFileExtension = '.code-workspace'; export const browseEllipsisWithIcon = `$(folder) ${localize('browseEllipsis', "Browse...")}`; export const selectProjectLocation = localize('selectProjectLocation', "Select project location"); export const sdkStyleProject = localize('sdkStyleProject', 'SDK-style project (Preview)'); @@ -316,8 +339,9 @@ export const SdkLearnMorePlaceholder = localize('sdkLearnMorePlaceholder', "Clic export const ProjectParentDirectoryNotExistError = (location: string): string => { return localize('dataworkspace.projectParentDirectoryNotExistError', "The selected project location '{0}' does not exist or is not a directory.", location); }; export const ProjectDirectoryAlreadyExistError = (projectName: string, location: string): string => { return localize('dataworkspace.projectDirectoryAlreadyExistError', "There is already a directory named '{0}' in the selected location: '{1}'.", projectName, location); }; -// Update Project From Database dialog strings +//#endregion +//#region Update Project From Database dialog strings export const updateProjectFromDatabaseDialogName = localize('updateProjectFromDatabaseDialogName', "Update project from database"); export const updateText = localize('updateText', "Update"); export const noSqlProjFile = localize('noSqlProjFile', "The selected project file does not exist"); @@ -329,15 +353,18 @@ export const updateActionRadioButtonLabel = localize('updateActionRadiButtonLabe export const actionLabel = localize('actionLabel', "Action"); export const applyConfirmation: string = localize('applyConfirmation', "Are you sure you want to update the target project?"); -// Update project from database +//#endregion +//#region Update project from database export const applySuccess = localize('applySuccess', "Project was successfully updated."); export const equalComparison = localize('equalComparison', "The project is already up to date with the database."); export function applyError(errorMessage: string): string { return localize('applyError', "There was an error updating the project: {0}", errorMessage); } export function updatingProjectFromDatabase(projectName: string, databaseName: string): string { return localize('updatingProjectFromDatabase', "Updating {0} from {1}...", projectName, databaseName); } -// Error messages +//#endregion +//#region Error messages +export function errorPrefix(errorMessage: string): string { return localize('errorPrefix', "Error: {0}", errorMessage); } export function compareErrorMessage(errorMessage: string): string { return localize('schemaCompare.compareErrorMessage', "Schema Compare failed: {0}", errorMessage ? errorMessage : 'Unknown'); } export const multipleSqlProjFiles = localize('multipleSqlProjFilesSelected', "Multiple .sqlproj files selected; please select only one."); export const noSqlProjFiles = localize('noSqlProjFilesSelected', "No .sqlproj file selected; please select one."); @@ -391,7 +418,11 @@ export function unableToFindSqlCmdVariable(variableName: string) { return locali export function unableToFindDatabaseReference(reference: string) { return localize('unableToFindReference', "Unable to find database reference {0}", reference); } export function invalidGuid(guid: string) { return localize('invalidGuid', "Specified GUID is invalid: {0}", guid); } export function invalidTargetPlatform(targetPlatform: string, supportedTargetPlatforms: string[]) { return localize('invalidTargetPlatform', "Invalid target platform: {0}. Supported target platforms: {1}", targetPlatform, supportedTargetPlatforms.toString()); } -export function errorReadingProject(section: string, path: string) { return localize('errorReadingProjectGuid', "Error trying to read {0} of project '{1}'", section, path); } +export function errorReadingProject(section: string, path: string, error?: string) { return localize('errorReadingProjectGuid', "Error trying to read {0} of project '{1}'. {2}", section, path, error); } +export function errorAddingDatabaseReference(referenceName: string, error: string) { return localize('errorAddingDatabaseReference', "Error adding database reference to {0}. Error: {1}", referenceName, error); } +export function errorNotSupportedInVsCode(actionDescription: string) { return localize('errorNotSupportedInVsCode', "Error: {0} is not currently supported in SQL Database Projects for VS Code.", actionDescription); } + +//#endregion // Action types export const deleteAction = localize('deleteAction', 'Delete'); @@ -401,8 +432,7 @@ export const excludeAction = localize('excludeAction', 'Exclude'); export const fileObject = localize('fileObject', "file"); export const folderObject = localize('folderObject', "folder"); -// Project script types - +//#region Project script types export const folderFriendlyName = localize('folderFriendlyName', "Folder"); export const scriptFriendlyName = localize('scriptFriendlyName', "Script"); export const tableFriendlyName = localize('tableFriendlyName', "Table"); @@ -415,18 +445,21 @@ export const externalStreamingJobFriendlyName = localize('externalStreamingJobFr export const preDeployScriptFriendlyName = localize('preDeployScriptFriendlyName', "Script.PreDeployment"); export const postDeployScriptFriendlyName = localize('postDeployScriptFriendlyName', "Script.PostDeployment"); -// Build +//#endregion +//#region Build export const DotnetInstallationConfirmation: string = localize('sqlDatabaseProjects.DotnetInstallationConfirmation', "The .NET SDK cannot be located. Project build will not work. Please install .NET 6 SDK or higher or update the .NET SDK location in settings if already installed."); export function NetCoreSupportedVersionInstallationConfirmation(installedVersion: string) { return localize('sqlDatabaseProjects.NetCoreSupportedVersionInstallationConfirmation', "Currently installed .NET SDK version is {0}, which is not supported. Project build will not work. Please install .NET 6 SDK or higher or update the .NET SDK supported version location in settings if already installed.", installedVersion); } export const UpdateDotnetLocation: string = localize('sqlDatabaseProjects.UpdateDotnetLocation', "Update Location"); export const projectsOutputChannel = localize('sqlDatabaseProjects.outputChannel', "Database Projects"); +//#endregion + // Prompt buttons export const Install: string = localize('sqlDatabaseProjects.Install', "Install"); export const DoNotAskAgain: string = localize('sqlDatabaseProjects.doNotAskAgain', "Don't Ask Again"); -// SqlProj file XML names +//#region SqlProj file XML names export const ItemGroup = 'ItemGroup'; export const Build = 'Build'; export const Folder = 'Folder'; @@ -486,11 +519,9 @@ export const ProjectReferenceElement = localize('projectReferenceElement', "Proj export const DacpacReferenceElement = localize('dacpacReferenceElement', "Dacpac reference"); export const PublishProfileElements = localize('publishProfileElements', "Publish profile elements"); -/** Name of the property item in the project file that defines default database collation. */ -export const DefaultCollationProperty = 'DefaultCollation'; +//#endregion + -/** Default database collation to use when none is specified in the project */ -export const DefaultCollation = 'SQL_Latin1_General_CP1_CI_AS'; /** * Well-known database source values that are allowed to be sent in telemetry. @@ -500,34 +531,8 @@ export const DefaultCollation = 'SQL_Latin1_General_CP1_CI_AS'; */ export const WellKnownDatabaseSources = ['dsct-oracle-to-ms-sql']; -// SqlProj File targets -export const NetCoreTargets = '$(NETCoreTargetsPath)\\Microsoft.Data.Tools.Schema.SqlTasks.targets'; -export const SqlDbTargets = '$(SQLDBExtensionsRefPath)\\Microsoft.Data.Tools.Schema.SqlTasks.targets'; -export const MsBuildtargets = '$(MSBuildExtensionsPath)\\Microsoft\\VisualStudio\\v$(VisualStudioVersion)\\SSDT\\Microsoft.Data.Tools.Schema.SqlTasks.targets'; -export const NetCoreCondition = '\'$(NetCoreBuild)\' == \'true\''; -export const NotNetCoreCondition = '\'$(NetCoreBuild)\' != \'true\''; -export const SqlDbPresentCondition = '\'$(SQLDBExtensionsRefPath)\' != \'\''; -export const SqlDbNotPresentCondition = '\'$(SQLDBExtensionsRefPath)\' == \'\''; -export const RoundTripSqlDbPresentCondition = '\'$(NetCoreBuild)\' != \'true\' AND \'$(SQLDBExtensionsRefPath)\' != \'\''; -export const RoundTripSqlDbNotPresentCondition = '\'$(NetCoreBuild)\' != \'true\' AND \'$(SQLDBExtensionsRefPath)\' == \'\''; -export const DacpacRootPath = '$(DacPacRootPath)'; -export const ProjJsonToClean = '$(BaseIntermediateOutputPath)\\project.assets.json'; -export const EmptyConfigurationCondition = '\'$(Configuration)\' == \'\''; -export const EmptyPlatformCondition = '\'$(Platform)\' == \'\''; -export function ConfigurationPlatformCondition(configuration: string, platform: string) { return `'$(Configuration)|$(Platform)' == '${configuration}|${platform}'`; } - export function defaultOutputPath(configuration: string) { return path.join('.', 'bin', configuration); } -// Sqlproj VS property conditions -export const VSVersionCondition = '\'$(VisualStudioVersion)\' == \'\''; -export const SsdtExistsCondition = '\'$(SSDTExists)\' == \'\''; -export const targetsExistsCondition = 'Exists(\'$(MSBuildExtensionsPath)\\Microsoft\\VisualStudio\\v$(VisualStudioVersion)\\SSDT\\Microsoft.Data.Tools.Schema.SqlTasks.targets\')'; - -// SqlProj Reference Assembly Information -export const NETFrameworkAssembly = 'Microsoft.NETFramework.ReferenceAssemblies'; -export const VersionNumber = '1.0.0'; -export const All = 'All'; - /** * Path separator to use within SqlProj file for `Include`, `Exclude`, etc. attributes. * This matches Windows path separator, as expected by SSDT. @@ -538,7 +543,7 @@ export const SqlProjPathSeparator = '\\'; export const targetDatabaseName = 'TargetDatabaseName'; export const targetConnectionString = 'TargetConnectionString'; -// SQL connection string components +//#region SQL connection string components export const initialCatalogSetting = 'Initial Catalog'; export const dataSourceSetting = 'Data Source'; export const integratedSecuritySetting = 'Integrated Security'; @@ -551,8 +556,9 @@ export const trustServerCertificateSetting = 'Trust Server Certificate'; export const hostnameInCertificateSetting = 'Host Name in Certificate'; export const azureAddAccount = localize('azureAddAccount', "Add an Account..."); +//#endregion -// Tree item types +//#region Tree item types export enum DatabaseProjectItemType { project = 'databaseProject.itemType.project', legacyProject = 'databaseProject.itemType.legacyProject', @@ -572,7 +578,9 @@ export enum DatabaseProjectItemType { publishProfile = 'databaseProject.itemType.file.publishProfile' } -// AutoRest +//#endregion + +//#region AutoRest export const autorestPostDeploymentScriptName = 'PostDeploymentScript.sql'; export const nodeButNotAutorestFound = localize('nodeButNotAutorestFound', "Autorest tool not found in system path, but found Node.js. Prompting user for how to proceed. Execute 'npm install autorest -g' to install permanently and avoid this message."); export const nodeNotFound = localize('nodeNotFound', "Neither Autorest nor Node.js (npx) found in system path. Please install Node.js for Autorest generation to work."); @@ -590,6 +598,7 @@ export function multipleMostDeploymentScripts(count: number) { return localize(' export const specSelectionText = localize('specSelectionText', "OpenAPI/Swagger spec"); export const autorestProjectName = localize('autorestProjectName', "New SQL project name"); export function generatingProjectFromAutorest(specName: string) { return localize('generatingProjectFromAutorest', "Generating new SQL project from {0}... Check output window for details.", specName); } +//#endregion // System dbs export const systemDbs = ['master', 'msdb', 'tempdb', 'model']; @@ -598,8 +607,9 @@ export const systemDbs = ['master', 'msdb', 'tempdb', 'model']; export const sameDatabaseExampleUsage = 'SELECT * FROM [Schema1].[Table1]'; export function differentDbSameServerExampleUsage(db: string) { return `SELECT * FROM [${db}].[Schema1].[Table1]`; } export function differentDbDifferentServerExampleUsage(server: string, db: string) { return `SELECT * FROM [${server}].[${db}].[Schema1].[Table1]`; } +//#endregion -// Target platforms +//#region Target platforms export const targetPlatformToVersion: Map = new Map([ [SqlTargetPlatform.sqlServer2012, '110'], [SqlTargetPlatform.sqlServer2014, '120'], @@ -635,33 +645,44 @@ export function getTargetPlatformFromVersion(version: string): string { return Array.from(targetPlatformToVersion.keys()).filter(k => targetPlatformToVersion.get(k) === version)[0]; } +//#endregion + export enum PublishTargetType { existingServer = 'existingServer', docker = 'docker', newAzureServer = 'newAzureServer' } -// Configuration keys +//#region Configuration keys export const CollapseProjectNodesKey = 'collapseProjectNodes'; export const microsoftBuildSqlVersionKey = 'microsoftBuildSqlVersion'; export const enablePreviewFeaturesKey = 'enablePreviewFeatures'; -// httpClient +//#endregion + +//#region httpClient export const downloadError = localize('downloadError', "Download error"); export const downloadProgress = localize('downloadProgress', "Download progress"); export const downloading = localize('downloading', "Downloading"); -// buildHelper +//#endregion + +//#region buildHelper export const downloadingDacFxDlls = localize('downloadingDacFxDlls', "Downloading Microsoft.Build.Sql nuget to get build DLLs"); export function downloadingFromTo(from: string, to: string) { return localize('downloadingFromTo', "Downloading from {0} to {1}", from, to); } export function extractingDacFxDlls(location: string) { return localize('extractingDacFxDlls', "Extracting DacFx build DLLs to {0}", location); } export function errorDownloading(url: string, error: string) { return localize('errorDownloading', "Error downloading {0}. Error: {1}", url, error); } export function errorExtracting(path: string, error: string) { return localize('errorExtracting', "Error extracting files from {0}. Error: {1}", path, error); } -// move +//#endregion + +//#region move export const onlyMoveSqlFilesSupported = localize('onlyMoveSqlFilesSupported', "Only moving .sql files is supported"); export const movingFilesBetweenProjectsNotSupported = localize('movingFilesBetweenProjectsNotSupported', "Moving files between projects is not supported"); export function errorMovingFile(source: string, destination: string, error: string) { return localize('errorMovingFile', "Error when moving file from {0} to {1}. Error: {2}", source, destination, error); } export function moveConfirmationPrompt(source: string, destination: string) { return localize('moveConfirmationPrompt', "Are you sure you want to move {0} to {1}?", source, destination); } export const move = localize('Move', "Move"); export function errorRenamingFile(source: string, destination: string, error: string) { return localize('errorRenamingFile', "Error when renaming file from {0} to {1}. Error: {2}", source, destination, error); } +export const unhandledMoveNode = localize('unhandledMoveNode', "Unhandled node type for move"); + +//#endregion diff --git a/extensions/sql-database-projects/src/common/telemetry.ts b/extensions/sql-database-projects/src/common/telemetry.ts index 9eb38ef47c..3697374f18 100644 --- a/extensions/sql-database-projects/src/common/telemetry.ts +++ b/extensions/sql-database-projects/src/common/telemetry.ts @@ -34,9 +34,6 @@ export enum TelemetryActions { build = 'build', updateProjectForRoundtrip = 'updateProjectForRoundtrip', changePlatformType = 'changePlatformType', - updateSystemDatabaseReferencesInProjFile = 'updateSystemDatabaseReferencesInProjFile', - startAddSqlBinding = 'startAddSqlBinding', - finishAddSqlBinding = 'finishAddSqlBinding', createProjectFromDatabase = 'createProjectFromDatabase', updateProjectFromDatabase = 'updateProjectFromDatabase', publishToContainer = 'publishToContainer', diff --git a/extensions/sql-database-projects/src/common/utils.ts b/extensions/sql-database-projects/src/common/utils.ts index c5f6c1f400..ed82085d3a 100644 --- a/extensions/sql-database-projects/src/common/utils.ts +++ b/extensions/sql-database-projects/src/common/utils.ts @@ -152,6 +152,19 @@ export function convertSlashesForSqlProj(filePath: string): string { : filePath; } +/** + * Converts a SystemDatabase enum to its string value + * @param systemDb + * @returns + */ +export function systemDatabaseToString(systemDb: mssql.SystemDatabase): string { + if (systemDb === mssql.SystemDatabase.Master) { + return constants.master; + } else { + return constants.msdb; + } +} + /** * Read SQLCMD variables from xmlDoc and return them * @param xmlDoc xml doc to read SQLCMD variables from. Format must be the same that sqlproj and publish profiles use @@ -303,14 +316,15 @@ export async function getSchemaCompareService(): Promise } } -export async function getSqlProjectsService(): Promise { +export async function getSqlProjectsService(): Promise { if (getAzdataApi()) { const ext = vscode.extensions.getExtension(mssql.extension.name) as vscode.Extension; const api = await ext.activate(); return api.sqlProjects; } else { - const api = await getVscodeMssqlApi(); - return api.sqlProjects; + throw new Error(constants.errorNotSupportedInVsCode('SqlProjectService')); + // const api = await getVscodeMssqlApi(); + // return api.sqlProjects; } } @@ -785,3 +799,33 @@ export function isPublishProfile(fileName: string): boolean { const hasPublishExtension = fileName.trim().toLowerCase().endsWith(constants.publishProfileExtension); return hasPublishExtension; } + +/** + * Checks to see if a file exists at absoluteFilePath, and writes contents if it doesn't. + * If either the file already exists and contents is specified or the file doesn't exist and contents is blank, + * then an exception is thrown. + * @param absoluteFilePath + * @param contents + */ +export async function ensureFileExists(absoluteFilePath: string, contents?: string): Promise { + if (contents) { + // Create the file if contents were passed in and file does not exist yet + await fs.mkdir(path.dirname(absoluteFilePath), { recursive: true }); + + try { + await fs.writeFile(absoluteFilePath, contents, { flag: 'wx' }); + } catch (error) { + if (error.code === 'EEXIST') { + // Throw specialized error, if file already exists + throw new Error(constants.fileAlreadyExists(path.parse(absoluteFilePath).name)); + } + + throw error; + } + } else { + // If no contents were provided, then check that file already exists + if (!await exists(absoluteFilePath)) { + throw new Error(constants.noFileExist(absoluteFilePath)); + } + } +} diff --git a/extensions/sql-database-projects/src/controllers/mainController.ts b/extensions/sql-database-projects/src/controllers/mainController.ts index d267569eb3..31546b2348 100644 --- a/extensions/sql-database-projects/src/controllers/mainController.ts +++ b/extensions/sql-database-projects/src/controllers/mainController.ts @@ -82,7 +82,6 @@ export default class MainController implements vscode.Disposable { this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.addDatabaseReference', async (node: WorkspaceTreeItem) => { return this.projectsController.addDatabaseReference(node); })); this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.openContainingFolder', async (node: WorkspaceTreeItem) => { return this.projectsController.openContainingFolder(node); })); this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.editProjectFile', async (node: WorkspaceTreeItem) => { return this.projectsController.editProjectFile(node); })); - this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.convertToSdkStyleProject', async (node: WorkspaceTreeItem) => { return this.projectsController.convertToSdkStyleProject(node); })); this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.delete', async (node: WorkspaceTreeItem) => { return this.projectsController.delete(node); })); this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.exclude', async (node: WorkspaceTreeItem) => { return this.projectsController.exclude(node); })); this.context.subscriptions.push(vscode.commands.registerCommand('sqlDatabaseProjects.rename', async (node: WorkspaceTreeItem) => { return this.projectsController.rename(node); })); diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index f1902e60a1..dae6e2ab57 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -17,9 +17,9 @@ import * as mssqlVscode from 'vscode-mssql'; import { promises as fs } from 'fs'; import { PublishDatabaseDialog } from '../dialogs/publishDatabaseDialog'; -import { Project, reservedProjectFolders } from '../models/project'; +import { Project } from '../models/project'; import { SqlDatabaseProjectTreeViewProvider } from './databaseProjectTreeViewProvider'; -import { FolderNode, FileNode, SqlObjectFileNode, PreDeployNode, PostDeployNode } from '../models/tree/fileFolderTreeItem'; +import { FolderNode, FileNode } from '../models/tree/fileFolderTreeItem'; import { BaseProjectTreeItem } from '../models/tree/baseTreeItem'; import { ImportDataModel } from '../models/api/import'; import { NetCoreTool, DotNetError } from '../tools/netcoreTool'; @@ -201,8 +201,9 @@ export class ProjectsController { const projectStyle = creationParams.sdkStyle ? mssql.ProjectType.SdkStyle : mssql.ProjectType.LegacyStyle; await (sqlProjectsService as mssql.ISqlProjectsService).createProject(newProjFilePath, projectStyle, targetPlatform); } else { - const projectStyle = creationParams.sdkStyle ? mssqlVscode.ProjectType.SdkStyle : mssqlVscode.ProjectType.LegacyStyle; - await (sqlProjectsService as mssqlVscode.ISqlProjectsService).createProject(newProjFilePath, projectStyle, targetPlatform); + throw new Error(constants.errorNotSupportedInVsCode('createProject')); + //const projectStyle = creationParams.sdkStyle ? mssqlVscode.ProjectType.SdkStyle : mssqlVscode.ProjectType.LegacyStyle; + //await (sqlProjectsService as mssqlVscode.ISqlProjectsService).createProject(newProjFilePath, projectStyle, targetPlatform); } await this.addTemplateFiles(newProjFilePath, creationParams.projectTypeId); @@ -223,19 +224,34 @@ export class ProjectsController { if (projectTypeId === constants.edgeSqlDatabaseProjectTypeId) { const project = await Project.openProject(newProjFilePath); - await this.createFileFromTemplate(project, templates.get(ItemType.table), 'DataTable.sql', { 'OBJECT_NAME': 'DataTable' }); - await this.createFileFromTemplate(project, templates.get(ItemType.dataSource), 'EdgeHubInputDataSource.sql', { 'OBJECT_NAME': 'EdgeHubInputDataSource', 'LOCATION': 'edgehub://' }); - await this.createFileFromTemplate(project, templates.get(ItemType.dataSource), 'SqlOutputDataSource.sql', { 'OBJECT_NAME': 'SqlOutputDataSource', 'LOCATION': 'sqlserver://tcp:.,1433' }); - await this.createFileFromTemplate(project, templates.get(ItemType.fileFormat), 'StreamFileFormat.sql', { 'OBJECT_NAME': 'StreamFileFormat' }); - await this.createFileFromTemplate(project, templates.get(ItemType.externalStream), 'EdgeHubInputStream.sql', { 'OBJECT_NAME': 'EdgeHubInputStream', 'DATA_SOURCE_NAME': 'EdgeHubInputDataSource', 'LOCATION': 'input', 'OPTIONS': ',\n\tFILE_FORMAT = StreamFileFormat' }); - await this.createFileFromTemplate(project, templates.get(ItemType.externalStream), 'SqlOutputStream.sql', { 'OBJECT_NAME': 'SqlOutputStream', 'DATA_SOURCE_NAME': 'SqlOutputDataSource', 'LOCATION': 'TSQLStreaming.dbo.DataTable', 'OPTIONS': '' }); - await this.createFileFromTemplate(project, templates.get(ItemType.externalStreamingJob), 'EdgeStreamingJob.sql', { 'OBJECT_NAME': 'EdgeStreamingJob' }); + await this.addFileToProjectFromTemplate(project, templates.get(ItemType.table), 'DataTable.sql', { 'OBJECT_NAME': 'DataTable' }); + await this.addFileToProjectFromTemplate(project, templates.get(ItemType.dataSource), 'EdgeHubInputDataSource.sql', { 'OBJECT_NAME': 'EdgeHubInputDataSource', 'LOCATION': 'edgehub://' }); + await this.addFileToProjectFromTemplate(project, templates.get(ItemType.dataSource), 'SqlOutputDataSource.sql', { 'OBJECT_NAME': 'SqlOutputDataSource', 'LOCATION': 'sqlserver://tcp:.,1433' }); + await this.addFileToProjectFromTemplate(project, templates.get(ItemType.fileFormat), 'StreamFileFormat.sql', { 'OBJECT_NAME': 'StreamFileFormat' }); + await this.addFileToProjectFromTemplate(project, templates.get(ItemType.externalStream), 'EdgeHubInputStream.sql', { 'OBJECT_NAME': 'EdgeHubInputStream', 'DATA_SOURCE_NAME': 'EdgeHubInputDataSource', 'LOCATION': 'input', 'OPTIONS': ',\n\tFILE_FORMAT = StreamFileFormat' }); + await this.addFileToProjectFromTemplate(project, templates.get(ItemType.externalStream), 'SqlOutputStream.sql', { 'OBJECT_NAME': 'SqlOutputStream', 'DATA_SOURCE_NAME': 'SqlOutputDataSource', 'LOCATION': 'TSQLStreaming.dbo.DataTable', 'OPTIONS': '' }); + await this.addFileToProjectFromTemplate(project, templates.get(ItemType.externalStreamingJob), 'EdgeStreamingJob.sql', { 'OBJECT_NAME': 'EdgeStreamingJob' }); } } - private async createFileFromTemplate(project: Project, itemType: templates.ProjectScriptType, relativePath: string, expansionMacros: Record): Promise { + private async addFileToProjectFromTemplate(project: ISqlProject, itemType: templates.ProjectScriptType, relativePath: string, expansionMacros: Record): Promise { const newFileText = templates.macroExpansion(itemType.templateScript, expansionMacros); - await project.addScriptItem(relativePath, newFileText, itemType.type); + const absolutePath = path.join(project.projectFolderPath, relativePath) + await utils.ensureFileExists(absolutePath, newFileText); + + switch (itemType.type) { + case ItemType.preDeployScript: + await project.addPreDeploymentScript(relativePath); + break; + case ItemType.postDeployScript: + await project.addPostDeploymentScript(relativePath); + break; + default: // a normal SQL object script + await project.addSqlObjectScript(relativePath); + break; + } + + return absolutePath; } //#endregion @@ -266,7 +282,7 @@ export class ProjectsController { } // get dlls and targets file needed for building for legacy style projects - if (!project.isSdkStyleProject) { + if (project.sqlProjStyle === mssql.ProjectType.LegacyStyle) { const result = await this.buildHelper.createBuildDirFolder(this._outputChannel); if (!result) { @@ -278,7 +294,7 @@ export class ProjectsController { const options: ShellCommandOptions = { commandTitle: 'Build', workingDirectory: project.projectFolderPath, - argument: this.buildHelper.constructBuildArguments(project.projectFilePath, this.buildHelper.extensionBuildDirPath, project.isSdkStyleProject) + argument: this.buildHelper.constructBuildArguments(project.projectFilePath, this.buildHelper.extensionBuildDirPath, project.sqlProjStyle) }; try { @@ -640,7 +656,7 @@ export class ProjectsController { throw new Error(constants.folderAlreadyExists(path.parse(absoluteFolderPath).name)); } - await project.addFolderItem(relativeFolderPath); + await project.addFolder(relativeFolderPath); this.refreshProjectsTree(treeNode); } catch (err) { void vscode.window.showErrorMessage(utils.getErrorMessage(err)); @@ -669,7 +685,7 @@ export class ProjectsController { } public isReservedFolder(absoluteFolderPath: string, projectFolderPath: string): boolean { - const sameName = reservedProjectFolders.find(f => f === path.parse(absoluteFolderPath).name) !== undefined; + const sameName = constants.reservedProjectFolders.find(f => f === path.parse(absoluteFolderPath).name) !== undefined; const sameLocation = path.parse(absoluteFolderPath).dir === projectFolderPath; return sameName && sameLocation; } @@ -707,7 +723,6 @@ export class ProjectsController { return; // user cancelled } - const newFileText = templates.macroExpansion(itemType.templateScript, { 'OBJECT_NAME': itemObjectName }); const relativeFilePath = path.join(relativePath, itemObjectName + constants.sqlFileExtension); const telemetryProps: Record = { itemType: itemType.type }; @@ -720,14 +735,14 @@ export class ProjectsController { } try { - const newEntry = await project.addScriptItem(relativeFilePath, newFileText, itemType.type); + const absolutePath = await this.addFileToProjectFromTemplate(project, itemType, relativeFilePath, { 'OBJECT_NAME': itemObjectName }); TelemetryReporter.createActionEvent(TelemetryViews.ProjectTree, TelemetryActions.addItemFromTree) .withAdditionalProperties(telemetryProps) .withAdditionalMeasurements(telemetryMeasurements) .send(); - await vscode.commands.executeCommand(constants.vscodeOpenCommand, newEntry.fsUri); + await vscode.commands.executeCommand(constants.vscodeOpenCommand, vscode.Uri.file(absolutePath)); treeDataProvider?.notifyTreeDataChanged(); } catch (err) { void vscode.window.showErrorMessage(utils.getErrorMessage(err)); @@ -772,7 +787,30 @@ export class ProjectsController { if (fileEntry) { TelemetryReporter.sendActionEvent(TelemetryViews.ProjectTree, TelemetryActions.excludeFromProject); - await project.exclude(fileEntry); + + switch (node.type) { + case constants.DatabaseProjectItemType.sqlObjectScript: + case constants.DatabaseProjectItemType.table: + case constants.DatabaseProjectItemType.externalStreamingJob: + await project.excludeSqlObjectScript(fileEntry.relativePath); + break; + case constants.DatabaseProjectItemType.folder: + // TODO: not yet supported in DacFx + //await project.excludeFolder(fileEntry.relativePath); + void vscode.window.showErrorMessage(constants.excludeFolderNotSupported); + break; + case constants.DatabaseProjectItemType.preDeploymentScript: + await project.excludePreDeploymentScript(fileEntry.relativePath); + break; + case constants.DatabaseProjectItemType.postDeploymentScript: + await project.excludePostDeploymentScript(fileEntry.relativePath); + break; + case constants.DatabaseProjectItemType.noneFile: + await project.excludeNoneItem(fileEntry.relativePath); + break; + default: + throw new Error(constants.unhandledExcludeType(node.type)); + } } else { TelemetryReporter.sendErrorEvent2(TelemetryViews.ProjectTree, TelemetryActions.excludeFromProject); void vscode.window.showErrorMessage(constants.unableToPerformAction(constants.excludeAction, node.relativeProjectUri.path)); @@ -802,35 +840,43 @@ export class ProjectsController { return; } - let success = false; + try { + if (node instanceof DatabaseReferenceTreeItem) { + const databaseReference = this.getDatabaseReference(project, node); - if (node instanceof DatabaseReferenceTreeItem) { - const databaseReference = this.getDatabaseReference(project, node); - - if (databaseReference) { - await project.deleteDatabaseReference(databaseReference); - success = true; + if (databaseReference) { + await project.deleteDatabaseReferenceByEntry(databaseReference); + } + } else if (node instanceof SqlCmdVariableTreeItem) { + await project.deleteSqlCmdVariable(node.friendlyName); + } else if (node instanceof FolderNode) { + await project.deleteFolder(node.entryKey); + } else if (node instanceof FileNode) { + switch (node.type) { + case constants.DatabaseProjectItemType.sqlObjectScript: + case constants.DatabaseProjectItemType.table: + case constants.DatabaseProjectItemType.externalStreamingJob: + await project.deleteSqlObjectScript(node.entryKey); + break; + case constants.DatabaseProjectItemType.preDeploymentScript: + await project.deletePreDeploymentScript(node.entryKey); + break; + case constants.DatabaseProjectItemType.postDeploymentScript: + await project.deletePostDeploymentScript(node.entryKey); + break; + case constants.DatabaseProjectItemType.noneFile: + await project.deleteNoneItem(node.entryKey); + break; + default: + throw new Error(constants.unhandledDeleteType(node.type)); + } } - } else if (node instanceof SqlCmdVariableTreeItem) { - const sqlProjectsService = await utils.getSqlProjectsService(); - const result = await sqlProjectsService.deleteSqlCmdVariable(project.projectFilePath, node.friendlyName); - success = result.success; - } else if (node instanceof FileNode || FolderNode) { - const fileEntry = this.getFileProjectEntry(project, node); - - if (fileEntry) { - await project.deleteFileFolder(fileEntry); - success = true; - } - } - - if (success) { TelemetryReporter.createActionEvent(TelemetryViews.ProjectTree, TelemetryActions.deleteObjectFromProject) .withAdditionalProperties({ objectType: node.constructor.name }) .send(); this.refreshProjectsTree(context); - } else { + } catch { TelemetryReporter.createErrorEvent2(TelemetryViews.ProjectTree, TelemetryActions.deleteObjectFromProject) .withAdditionalProperties({ objectType: node.constructor.name }) .send(); @@ -862,7 +908,7 @@ export class ProjectsController { const newFilePath = path.join(path.dirname(utils.getPlatformSafeFileEntryPath(file?.relativePath!)), `${newFileName}.sql`); - const renameResult = await this.move(node, node.projectFileUri.fsPath, newFilePath); + const renameResult = await project.move(node, newFilePath); if (renameResult?.success) { TelemetryReporter.sendActionEvent(TelemetryViews.ProjectTree, TelemetryActions.rename); @@ -885,11 +931,12 @@ export class ProjectsController { public async editSqlCmdVariable(context: dataworkspace.WorkspaceTreeItem): Promise { const node = context.element as SqlCmdVariableTreeItem; const project = await this.getProjectFromContext(node); - const originalValue = project.sqlCmdVariables[node.friendlyName]; // TODO: update to hookup with however sqlcmd vars work after swap + const variableName = node.friendlyName; + const originalValue = project.sqlCmdVariables[variableName]; const newValue = await vscode.window.showInputBox( { - title: constants.enterNewValueForVar(node.friendlyName), + title: constants.enterNewValueForVar(variableName), value: originalValue, ignoreFocusOut: true }); @@ -898,7 +945,8 @@ export class ProjectsController { return; } - // TODO: update value in sqlcmd variables after swap + await project.updateSqlCmdVariable(variableName, newValue) + this.refreshProjectsTree(context); } /** @@ -931,9 +979,7 @@ export class ProjectsController { return; } - // TODO: update after swap await project.addSqlCmdVariable(variableName, defaultValue); - this.refreshProjectsTree(context); } @@ -945,7 +991,7 @@ export class ProjectsController { const databaseReference = context as DatabaseReferenceTreeItem; if (databaseReference) { - return project.databaseReferences.find(r => r.databaseName === databaseReference.treeItem.label); + return project.databaseReferences.find(r => r.referenceName === databaseReference.treeItem.label); } return undefined; @@ -1068,35 +1114,6 @@ export class ProjectsController { } } - /** - * Converts a legacy style project to an SDK-style project - * @param context a treeItem in a project's hierarchy, to be used to obtain a Project - */ - public async convertToSdkStyleProject(context: dataworkspace.WorkspaceTreeItem): Promise { - const project = await this.getProjectFromContext(context); - - // confirm that user wants to update the project and knows the SSDT doesn't have support for displaying glob files yet - await vscode.window.showWarningMessage(constants.convertToSdkStyleConfirmation(project.projectFileName), { modal: true }, constants.yesString).then(async (result) => { - if (result === constants.yesString) { - const updateResult = await project.convertProjectToSdkStyle(); - void this.reloadProject(context); - - if (!updateResult) { - void vscode.window.showErrorMessage(constants.updatedToSdkStyleError(project.projectFileName)); - } else { - void this.reloadProject(context); - - // show message that project file can be simplified - const result = await vscode.window.showInformationMessage(constants.projectUpdatedToSdkStyle(project.projectFileName), constants.learnMore); - - if (result === constants.learnMore) { - void vscode.env.openExternal(vscode.Uri.parse(constants.sdkLearnMoreUrl!)); - } - } - } - }); - } - //#region database references /** @@ -1367,9 +1384,9 @@ export class ProjectsController { return; } - const fileFolderList: vscode.Uri[] | undefined = await this.getSqlFileList(projectInfo.newProjectFolder); + const scriptList: vscode.Uri[] | undefined = await this.getSqlFileList(projectInfo.newProjectFolder); - if (!fileFolderList || fileFolderList.length === 0) { + if (!scriptList || scriptList.length === 0) { void vscode.window.showInformationMessage(constants.noSqlFilesGenerated); this._outputChannel.show(); return; @@ -1386,12 +1403,15 @@ export class ProjectsController { const project = await Project.openProject(newProjFilePath); // 6. add generated files to SQL project - await project.addToProject(fileFolderList.filter(f => !f.fsPath.endsWith(constants.autorestPostDeploymentScriptName))); // Add generated file structure to the project - const postDeploymentScript: vscode.Uri | undefined = this.findPostDeploymentScript(fileFolderList); + const uriList = scriptList.filter(f => !f.fsPath.endsWith(constants.autorestPostDeploymentScriptName)) + const relativePaths = uriList.map(f => path.relative(project.projectFolderPath, f.path)); + await project.addSqlObjectScripts(relativePaths); // Add generated file structure to the project + + const postDeploymentScript: vscode.Uri | undefined = this.findPostDeploymentScript(scriptList); if (postDeploymentScript) { - await project.addScriptItem(path.relative(project.projectFolderPath, postDeploymentScript.fsPath), undefined, ItemType.postDeployScript); + await project.addPostDeploymentScript(path.relative(project.projectFolderPath, postDeploymentScript.fsPath)); } if (options?.doNotOpenInWorkspace !== true) { @@ -1576,10 +1596,12 @@ export class ProjectsController { .withAdditionalMeasurements({ durationMs: timeToExtract }) .send(); - let fileFolderList: vscode.Uri[] = model.extractTarget === mssql.ExtractTarget.file ? [vscode.Uri.file(model.filePath)] : await this.generateList(model.filePath); // Create a list of all the files and directories to be added to project + const scriptList: vscode.Uri[] = model.extractTarget === mssql.ExtractTarget.file ? [vscode.Uri.file(model.filePath)] : await this.generateScriptList(model.filePath); // Create a list of all the files to be added to project + + const relativePaths = scriptList.map(f => path.relative(project.projectFolderPath, f.path)); if (!model.sdkStyle) { - await project.addToProject(fileFolderList); // Add generated file structure to the project + await project.addSqlObjectScripts(relativePaths); // Add generated file structure to the project } // add project to workspace @@ -1611,19 +1633,19 @@ export class ProjectsController { } /** - * Generate a flat list of all files and folder under a folder. + * Generate a flat list of all scripts under a folder. * @param absolutePath absolute path to folder to generate the list of files from - * @returns array of uris of files and folders under the provided folder + * @returns array of uris of files under the provided folder */ - public async generateList(absolutePath: string): Promise { - let fileFolderList: vscode.Uri[] = []; + public async generateScriptList(absolutePath: string): Promise { + let fileList: vscode.Uri[] = []; if (!await utils.exists(absolutePath)) { if (await utils.exists(absolutePath + constants.sqlFileExtension)) { absolutePath += constants.sqlFileExtension; } else { void vscode.window.showErrorMessage(constants.cannotResolvePath(absolutePath)); - return fileFolderList; + return fileList; } } @@ -1635,19 +1657,18 @@ export class ProjectsController { const stat = await fs.stat(filepath); if (stat.isDirectory()) { - fileFolderList.push(vscode.Uri.file(filepath)); (await fs .readdir(filepath)) .forEach((f: string) => files.push(path.join(filepath, f))); } - else if (stat.isFile()) { - fileFolderList.push(vscode.Uri.file(filepath)); + else if (stat.isFile() && path.extname(filepath) === constants.sqlFileExtension) { + fileList.push(vscode.Uri.file(filepath)); } } } while (files.length !== 0); - return fileFolderList; + return fileList; } //#endregion @@ -1817,7 +1838,9 @@ export class ProjectsController { let toAdd: vscode.Uri[] = []; result.addedFiles.forEach((f: any) => toAdd.push(vscode.Uri.file(f))); - await project.addToProject(toAdd); + const relativePaths = toAdd.map(f => path.relative(project.projectFolderPath, f.path)); + + await project.addSqlObjectScripts(relativePaths); let toRemove: vscode.Uri[] = []; result.deletedFiles.forEach((f: any) => toRemove.push(vscode.Uri.file(f))); @@ -1825,7 +1848,7 @@ export class ProjectsController { let toRemoveEntries: FileProjectEntry[] = []; toRemove.forEach(f => toRemoveEntries.push(new FileProjectEntry(f, f.path.replace(projectPath + '\\', ''), EntryType.File))); - toRemoveEntries.forEach(async f => await project.exclude(f)); + toRemoveEntries.forEach(async f => await project.excludeSqlObjectScript(f.fsUri.fsPath)); await this.buildProject(project); } @@ -1843,6 +1866,7 @@ export class ProjectsController { */ public async moveFile(projectUri: vscode.Uri, source: any, target: dataworkspace.WorkspaceTreeItem): Promise { const sourceFileNode = source as FileNode; + const project = await this.getProjectFromContext(sourceFileNode); // only moving files is supported if (!sourceFileNode || !(sourceFileNode instanceof FileNode)) { @@ -1885,7 +1909,7 @@ export class ProjectsController { } // Move the file - const moveResult = await this.move(sourceFileNode, projectUri.fsPath, newPath); + const moveResult = await project.move(sourceFileNode, newPath); if (moveResult?.success) { TelemetryReporter.sendActionEvent(TelemetryViews.ProjectTree, TelemetryActions.move); @@ -1894,38 +1918,6 @@ export class ProjectsController { void vscode.window.showErrorMessage(constants.errorMovingFile(sourceFileNode.fileSystemUri.fsPath, newPath, utils.getErrorMessage(moveResult?.errorMessage))); } } - - /** - * Moves a file to a different location - * @param node Node being moved - * @param projectFilePath Full file path to .sqlproj - * @param destinationRelativePath path of the destination, relative to .sqlproj - */ - private async move(node: BaseProjectTreeItem, projectFilePath: string, destinationRelativePath: string): Promise { - // trim off the project folder at the beginning of the relative path stored in the tree - const projectRelativeUri = vscode.Uri.file(path.basename(projectFilePath, constants.sqlprojExtension)); - const originalRelativePath = utils.trimUri(projectRelativeUri, node.relativeProjectUri); - destinationRelativePath = utils.trimUri(projectRelativeUri, vscode.Uri.file(destinationRelativePath)); - - if (originalRelativePath === destinationRelativePath) { - return; - } - - const sqlProjectsService = await utils.getSqlProjectsService(); - - let result; - if (node instanceof SqlObjectFileNode) { - result = await sqlProjectsService.moveSqlObjectScript(projectFilePath, destinationRelativePath, originalRelativePath) - } else if (node instanceof PreDeployNode) { - result = await sqlProjectsService.movePreDeploymentScript(projectFilePath, destinationRelativePath, originalRelativePath) - } else if (node instanceof PostDeployNode) { - result = await sqlProjectsService.movePostDeploymentScript(projectFilePath, destinationRelativePath, originalRelativePath) - } - // TODO add support for renaming none scripts after those are added in STS - // TODO add support for renaming publish profiles when support is added in DacFx - - return result; - } } export interface NewProjectParams { diff --git a/extensions/sql-database-projects/src/dialogs/addDatabaseReferenceDialog.ts b/extensions/sql-database-projects/src/dialogs/addDatabaseReferenceDialog.ts index 2550bc550b..b04ad7d678 100644 --- a/extensions/sql-database-projects/src/dialogs/addDatabaseReferenceDialog.ts +++ b/extensions/sql-database-projects/src/dialogs/addDatabaseReferenceDialog.ts @@ -15,7 +15,8 @@ import { IconPathHelper } from '../common/iconHelper'; import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings, IProjectReferenceSettings } from '../models/IDatabaseReferenceSettings'; import { Deferred } from '../common/promise'; import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry'; -import { SystemDatabase } from '../models/projectEntry'; +import { SystemDatabase } from 'mssql'; +import { DbServerValues, ensureSetOrDefined, populateResultWithVars } from './utils'; export enum ReferenceType { project, @@ -151,32 +152,42 @@ export class AddDatabaseReferenceDialog { public async addReferenceClick(): Promise { let referenceSettings: ISystemDatabaseReferenceSettings | IDacpacReferenceSettings | IProjectReferenceSettings; - if (this.currentReferenceType === ReferenceType.project) { - referenceSettings = { - projectName: this.projectDropdown?.value, - projectGuid: '', - projectRelativePath: undefined, - databaseName: this.databaseNameTextbox?.value, - databaseVariable: this.databaseVariableTextbox?.value, - serverName: this.serverNameTextbox?.value, - serverVariable: this.serverVariableTextbox?.value, - suppressMissingDependenciesErrors: this.suppressMissingDependenciesErrorsCheckbox?.checked - }; - } else if (this.currentReferenceType === ReferenceType.systemDb) { - referenceSettings = { - databaseName: this.databaseNameTextbox?.value, + if (this.currentReferenceType === ReferenceType.systemDb) { + const systemDbRef: ISystemDatabaseReferenceSettings = { + databaseVariableLiteralValue: this.databaseNameTextbox?.value, systemDb: getSystemDatabase(this.systemDatabaseDropdown?.value), suppressMissingDependenciesErrors: this.suppressMissingDependenciesErrorsCheckbox?.checked }; - } else { // this.currentReferenceType === ReferenceType.dacpac - referenceSettings = { - databaseName: this.databaseNameTextbox?.value, - dacpacFileLocation: vscode.Uri.file(this.dacpacTextbox?.value), - databaseVariable: utils.removeSqlCmdVariableFormatting(this.databaseVariableTextbox?.value), - serverName: this.serverNameTextbox?.value, - serverVariable: utils.removeSqlCmdVariableFormatting(this.serverVariableTextbox?.value), - suppressMissingDependenciesErrors: this.suppressMissingDependenciesErrorsCheckbox?.checked + + referenceSettings = systemDbRef; + } else { + if (this.currentReferenceType === ReferenceType.project) { + const projRef: IProjectReferenceSettings = { + projectName: this.projectDropdown?.value, + projectGuid: '', + projectRelativePath: undefined, + suppressMissingDependenciesErrors: this.suppressMissingDependenciesErrorsCheckbox?.checked + }; + + referenceSettings = projRef; + } else { // this.currentReferenceType === ReferenceType.dacpac + const dacpacRef: IDacpacReferenceSettings = { + databaseName: ensureSetOrDefined(this.databaseNameTextbox?.value), + dacpacFileLocation: vscode.Uri.file(this.dacpacTextbox?.value), + suppressMissingDependenciesErrors: this.suppressMissingDependenciesErrorsCheckbox?.checked + }; + + referenceSettings = dacpacRef; + } + + const dbServerValues: DbServerValues = { + dbName: this.databaseNameTextbox?.value, + dbVariable: this.databaseVariableTextbox?.value, + serverName: this.serverNameTextbox?.value, + serverVariable: this.serverVariableTextbox?.value }; + + populateResultWithVars(referenceSettings, dbServerValues); } TelemetryReporter.createActionEvent(TelemetryViews.ProjectTree, TelemetryActions.addDatabaseReference) @@ -625,7 +636,7 @@ export function getSystemDbOptions(project: Project): string[] { } export function getSystemDatabase(name: string): SystemDatabase { - return name === constants.master ? SystemDatabase.master : SystemDatabase.msdb; + return name === constants.master ? SystemDatabase.Master : SystemDatabase.MSDB; } export async function promptDacpacLocation(): Promise { diff --git a/extensions/sql-database-projects/src/dialogs/addDatabaseReferenceQuickpick.ts b/extensions/sql-database-projects/src/dialogs/addDatabaseReferenceQuickpick.ts index b90accb48f..38ec9acf09 100644 --- a/extensions/sql-database-projects/src/dialogs/addDatabaseReferenceQuickpick.ts +++ b/extensions/sql-database-projects/src/dialogs/addDatabaseReferenceQuickpick.ts @@ -6,18 +6,14 @@ import path = require('path'); import * as vscode from 'vscode'; import * as constants from '../common/constants'; -import { getSqlProjectsInWorkspace, isValidSqlCmdVariableName, removeSqlCmdVariableFormatting } from '../common/utils'; +import { getSqlProjectsInWorkspace, isValidSqlCmdVariableName } from '../common/utils'; +import { DbServerValues, populateResultWithVars } from './utils'; import { AddDatabaseReferenceSettings } from '../controllers/projectController'; import { IDacpacReferenceSettings, IProjectReferenceSettings, ISystemDatabaseReferenceSettings } from '../models/IDatabaseReferenceSettings'; import { Project } from '../models/project'; import { getSystemDatabase, getSystemDbOptions, promptDacpacLocation } from './addDatabaseReferenceDialog'; -interface DbServerValues { - dbName?: string, - dbVariable?: string, - serverName?: string, - serverVariable?: string -} + /** * Create flow for adding a database reference using only VS Code-native APIs such as QuickPick @@ -93,10 +89,8 @@ async function addProjectReference(otherProjectsInWorkspace: vscode.Uri[]): Prom // User cancelled return; } - referenceSettings.databaseName = dbServerValues.dbName; - referenceSettings.databaseVariable = dbServerValues.dbVariable; - referenceSettings.serverName = dbServerValues.serverName; - referenceSettings.serverVariable = dbServerValues.serverVariable; + + populateResultWithVars(referenceSettings, dbServerValues); // 7. Prompt suppress unresolved ref errors const suppressErrors = await promptSuppressUnresolvedRefErrors(); @@ -124,7 +118,7 @@ async function addSystemDatabaseReference(project: Project): Promise { diff --git a/extensions/sql-database-projects/src/dialogs/utils.ts b/extensions/sql-database-projects/src/dialogs/utils.ts index 96f19a4dc5..d2c798c939 100644 --- a/extensions/sql-database-projects/src/dialogs/utils.ts +++ b/extensions/sql-database-projects/src/dialogs/utils.ts @@ -9,6 +9,8 @@ import * as utils from '../common/utils'; import * as mssql from 'mssql'; import { HttpClient } from '../common/httpClient'; import { AgreementInfo, DockerImageInfo } from '../models/deploy/deployProfile'; +import { IUserDatabaseReferenceSettings } from '../models/IDatabaseReferenceSettings'; +import { removeSqlCmdVariableFormatting } from '../common/utils'; /** * Gets connection name from connection object if there is one, @@ -214,3 +216,32 @@ export function mapExtractTargetEnum(inputTarget: string): mssql.ExtractTarget { throw new Error(constants.extractTargetRequired); } } + +export interface DbServerValues { + dbName?: string, + dbVariable?: string, + serverName?: string, + serverVariable?: string +} + +export function populateResultWithVars(referenceSettings: IUserDatabaseReferenceSettings, dbServerValues: DbServerValues) { + if (dbServerValues.dbVariable) { + referenceSettings.databaseName = ensureSetOrDefined(dbServerValues.dbName); + referenceSettings.databaseVariable = ensureSetOrDefined(removeSqlCmdVariableFormatting(dbServerValues.dbVariable)); + referenceSettings.serverName = ensureSetOrDefined(dbServerValues.serverName); + referenceSettings.serverVariable = ensureSetOrDefined(removeSqlCmdVariableFormatting(dbServerValues.serverVariable)); + } else { + referenceSettings.databaseVariableLiteralValue = ensureSetOrDefined(dbServerValues.dbName); + } +} + +/** + * Returns undefined for settings that are an empty string, meaning they are unset + * @param setting + */ +export function ensureSetOrDefined(setting?: string): string | undefined { + if (!setting || setting.trim().length === 0) { + return undefined; + } + return setting; +} diff --git a/extensions/sql-database-projects/src/models/IDatabaseReferenceSettings.ts b/extensions/sql-database-projects/src/models/IDatabaseReferenceSettings.ts index 7263fd3996..7e2c3e6c39 100644 --- a/extensions/sql-database-projects/src/models/IDatabaseReferenceSettings.ts +++ b/extensions/sql-database-projects/src/models/IDatabaseReferenceSettings.ts @@ -3,11 +3,11 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { SystemDatabase } from 'mssql'; import { Uri } from 'vscode'; -import { SystemDatabase } from './projectEntry'; export interface IDatabaseReferenceSettings { - databaseName?: string; + databaseVariableLiteralValue?: string; suppressMissingDependenciesErrors: boolean; } @@ -15,18 +15,19 @@ export interface ISystemDatabaseReferenceSettings extends IDatabaseReferenceSett systemDb: SystemDatabase; } -export interface IDacpacReferenceSettings extends IDatabaseReferenceSettings { - dacpacFileLocation: Uri; +export interface IUserDatabaseReferenceSettings extends IDatabaseReferenceSettings { + databaseName?: string; databaseVariable?: string; serverName?: string; serverVariable?: string; } -export interface IProjectReferenceSettings extends IDatabaseReferenceSettings { +export interface IDacpacReferenceSettings extends IUserDatabaseReferenceSettings { + dacpacFileLocation: Uri; +} + +export interface IProjectReferenceSettings extends IUserDatabaseReferenceSettings { projectRelativePath: Uri | undefined; projectName: string; projectGuid: string; - databaseVariable?: string; - serverName?: string; - serverVariable?: string; } diff --git a/extensions/sql-database-projects/src/models/project.ts b/extensions/sql-database-projects/src/models/project.ts index 26cc229e35..4b0c1c3755 100644 --- a/extensions/sql-database-projects/src/models/project.ts +++ b/extensions/sql-database-projects/src/models/project.ts @@ -4,20 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import * as path from 'path'; -import * as xmldom from '@xmldom/xmldom'; import * as constants from '../common/constants'; import * as utils from '../common/utils'; -import * as xmlFormat from 'xml-formatter'; -import * as os from 'os'; -import * as UUID from 'vscode-languageclient/lib/utils/uuid'; +import type * as azdataType from 'azdata'; +import * as vscode from 'vscode'; +import * as mssql from 'mssql'; import { Uri, window } from 'vscode'; -import { EntryType, IDatabaseReferenceProjectEntry, IProjectEntry, ISqlProject, ItemType, SqlTargetPlatform } from 'sqldbproj'; -import { promises as fs } from 'fs'; +import { EntryType, IDatabaseReferenceProjectEntry, ISqlProject, ItemType } from 'sqldbproj'; import { DataSource } from './dataSources/dataSources'; import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings, IProjectReferenceSettings } from './IDatabaseReferenceSettings'; import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry'; -import { DacpacReferenceProjectEntry, FileProjectEntry, ProjectEntry, SqlCmdVariableProjectEntry, SqlProjectReferenceProjectEntry, SystemDatabase, SystemDatabaseReferenceProjectEntry } from './projectEntry'; +import { DacpacReferenceProjectEntry, FileProjectEntry, SqlProjectReferenceProjectEntry, SystemDatabaseReferenceProjectEntry } from './projectEntry'; +import { ResultStatus } from 'azdata'; +import { BaseProjectTreeItem } from './tree/baseTreeItem'; +import { NoneNode, PostDeployNode, PreDeployNode, PublishProfileNode, SqlObjectFileNode } from './tree/fileFolderTreeItem'; +import { GetFoldersResult, GetScriptsResult, ProjectType, SystemDatabase } from 'mssql'; /** * Represents the configuration based on the Configuration property in the sqlproj @@ -32,21 +34,31 @@ enum Configuration { * Class representing a Project, and providing functions for operating on it */ export class Project implements ISqlProject { + private sqlProjService!: mssql.ISqlProjectsService; + private _projectFilePath: string; private _projectFileName: string; private _projectGuid: string | undefined; private _files: FileProjectEntry[] = []; + private _folders: FileProjectEntry[] = []; private _dataSources: DataSource[] = []; - private _importedTargets: string[] = []; private _databaseReferences: IDatabaseReferenceProjectEntry[] = []; private _sqlCmdVariables: Record = {}; private _preDeployScripts: FileProjectEntry[] = []; private _postDeployScripts: FileProjectEntry[] = []; private _noneDeployScripts: FileProjectEntry[] = []; - private _isSdkStyleProject: boolean = false; // https://docs.microsoft.com/en-us/dotnet/core/project-sdk/overview + private _sqlProjStyle: ProjectType = ProjectType.SdkStyle; + private _isCrossPlatformCompatible: boolean = false; private _outputPath: string = ''; private _configuration: Configuration = Configuration.Debug; + private _databaseSource: string = ''; private _publishProfiles: FileProjectEntry[] = []; + private _defaultCollation: string = ''; + private _databaseSchemaProvider: string = ''; + + //#endregion + + //#region Public Properties public get dacpacOutputPath(): string { return path.join(this.outputPath, `${this._projectFileName}.dacpac`); @@ -72,12 +84,12 @@ export class Project implements ISqlProject { return this._files; } - public get dataSources(): DataSource[] { - return this._dataSources; + public get folders(): FileProjectEntry[] { + return this._folders; } - public get importedTargets(): string[] { - return this._importedTargets; + public get dataSources(): DataSource[] { + return this._dataSources; } public get databaseReferences(): IDatabaseReferenceProjectEntry[] { @@ -100,8 +112,12 @@ export class Project implements ISqlProject { return this._noneDeployScripts; } - public get isSdkStyleProject(): boolean { - return this._isSdkStyleProject; + public get sqlProjStyle(): ProjectType { + return this._sqlProjStyle; + } + + public get isCrossPlatformCompatible(): boolean { + return this._isCrossPlatformCompatible; } public get outputPath(): string { @@ -116,7 +132,7 @@ export class Project implements ISqlProject { return this._publishProfiles; } - private projFileXmlDoc: Document | undefined = undefined; + //#endregion constructor(projectFilePath: string) { this._projectFilePath = projectFilePath; @@ -125,11 +141,30 @@ export class Project implements ISqlProject { /** * Open and load a .sqlproj file + * @param projectFilePath + * @param promptIfNeedsUpdating whether or not to prompt the user if the project needs to be updated + * @param reload whether to reload the project from the project file + * @returns */ - public static async openProject(projectFilePath: string): Promise { + public static async openProject(projectFilePath: string, promptIfNeedsUpdating: boolean = false, reload: boolean = false): Promise { const proj = new Project(projectFilePath); + + proj.sqlProjService = await utils.getSqlProjectsService(); + + if (reload) { + // close the project in STS so that it will reload the project from the .sqlproj, rather than using the cached Project in STS + await proj.sqlProjService.closeProject(projectFilePath); + } + await proj.readProjFile(); - await proj.updateProjectForRoundTrip(); + + if (!proj.isCrossPlatformCompatible && promptIfNeedsUpdating) { + const result = await window.showWarningMessage(constants.updateProjectForRoundTrip(proj.projectFileName), constants.yesString, constants.noString); + + if (result === constants.yesString) { + await proj.updateProjectForRoundTrip(); + } + } return proj; } @@ -140,107 +175,66 @@ export class Project implements ISqlProject { public async readProjFile(): Promise { this.resetProject(); - const projFileText = await fs.readFile(this._projectFilePath); - this.projFileXmlDoc = new xmldom.DOMParser().parseFromString(projFileText.toString()); - - // check if this is an sdk style project https://docs.microsoft.com/en-us/dotnet/core/project-sdk/overview - this._isSdkStyleProject = this.CheckForSdkStyleProject(); + await this.readProjectProperties(); + await this.readSqlCmdVariables(); + await this.readDatabaseReferences(); // get pre and post deploy scripts specified in the sqlproj - this._preDeployScripts = this.readPreDeployScripts(); - this._postDeployScripts = this.readPostDeployScripts(); - this._noneDeployScripts = this.readNoneDeployScripts(); + await this.readPreDeployScripts(true); + await this.readPostDeployScripts(true); - // get files and folders - this._files = await this.readFilesInProject(); - this.files.push(...await this.readFolders()); + await this.readNoneItems(); // also populates list of publish profiles, determined by file extension - this._databaseReferences = this.readDatabaseReferences(); - this._importedTargets = this.readImportedTargets(); + await this.readFilesInProject(); // get SQL object scripts + await this.readFolders(); // get folders + } - // get publish profiles specified in the sqlproj - this._publishProfiles = this.readPublishProfiles(); + //#region Reader helpers - // find all SQLCMD variables to include - try { - this._sqlCmdVariables = utils.readSqlCmdVariables(this.projFileXmlDoc, false); - } catch (e) { - void window.showErrorMessage(constants.errorReadingProject(constants.sqlCmdVariables, this.projectFilePath)); - console.error(utils.getErrorMessage(e)); + private async readProjectProperties(): Promise { + const result = await this.sqlProjService.getProjectProperties(this.projectFilePath); + this.throwIfFailed(result); + + this._projectGuid = result.projectGuid; + + switch (result.configuration.toLowerCase()) { + case Configuration.Debug.toString().toLowerCase(): + this._configuration = Configuration.Debug; + break; + case Configuration.Release.toString().toLowerCase(): + this._configuration = Configuration.Release; + break; + default: + this._configuration = Configuration.Output; // if the configuration doesn't match release or debug, the dacpac will get created in ./bin/Output } - // get projectGUID - try { - this._projectGuid = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ProjectGuid)[0].childNodes[0].nodeValue!; - } catch (e) { - // if no project guid, add a new one - this._projectGuid = UUID.generateUuid(); - const newProjectGuidNode = this.projFileXmlDoc!.createElement(constants.ProjectGuid); - const newProjectGuidTextNode = this.projFileXmlDoc!.createTextNode(`{${this._projectGuid}}`); - newProjectGuidNode.appendChild(newProjectGuidTextNode); - this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.PropertyGroup)[0]?.appendChild(newProjectGuidNode); - await this.serializeToProjFile(this.projFileXmlDoc); + this._outputPath = path.isAbsolute(result.outputPath) ? result.outputPath : path.join(this.projectFolderPath, utils.getPlatformSafeFileEntryPath(result.outputPath)); + this._databaseSource = result.databaseSource ?? ''; + this._defaultCollation = result.defaultCollation; + this._databaseSchemaProvider = result.databaseSchemaProvider; + this._sqlProjStyle = result.projectStyle; + + await this.readCrossPlatformCompatibility(); + } + + private async readCrossPlatformCompatibility(): Promise { + const result = await this.sqlProjService.getCrossPlatformCompatibility(this.projectFilePath) + this.throwIfFailed(result); + + this._isCrossPlatformCompatible = result.isCrossPlatformCompatible; + } + + private async readSqlCmdVariables(): Promise { + const sqlcmdVariablesResult = await this.sqlProjService.getSqlCmdVariables(this.projectFilePath); + + if (!sqlcmdVariablesResult.success && sqlcmdVariablesResult.errorMessage) { + throw new Error(constants.errorReadingProject(constants.sqlCmdVariables, this.projectFilePath, sqlcmdVariablesResult.errorMessage)); } - // get configuration - const configurationNodes = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.Configuration); - if (configurationNodes.length > 0) { - const configuration = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.Configuration)[0].childNodes[0].nodeValue!; - switch (configuration.toLowerCase()) { - case Configuration.Debug.toString().toLowerCase(): - this._configuration = Configuration.Debug; - break; - case Configuration.Release.toString().toLowerCase(): - this._configuration = Configuration.Release; - break; - default: - // if the configuration doesn't match release or debug, the dacpac will get created in ./bin/Output - this._configuration = Configuration.Output; - } - } else { - // If configuration isn't specified in .sqlproj, set it to the default debug - this._configuration = Configuration.Debug; - } + this._sqlCmdVariables = {}; - // get platform - const platformNodes = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.Platform); - let platform = ''; - if (platformNodes.length > 0) { - for (let i = 0; i < platformNodes.length; i++) { - const condition = platformNodes[i].getAttribute(constants.Condition); - if (condition?.trim() === constants.EmptyPlatformCondition.trim()) { - platform = platformNodes[i].childNodes[0].nodeValue ?? ''; - break; - } - } - } else { - platform = constants.AnyCPU; - } - - // get output path - let outputPath; - const outputPathNodes = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.OutputPath); - if (outputPathNodes.length > 0) { - // go through all the OutputPath nodes and use the last one in the .sqlproj that the condition matches - for (let i = 0; i < outputPathNodes.length; i++) { - // check if parent has a condition - const parent = outputPathNodes[i].parentNode as Element; - const condition = parent?.getAttribute(constants.Condition); - - // only handle the default conditions format that are there when creating a sqlproj in VS or ADS - if (condition?.toLowerCase().trim() === constants.ConfigurationPlatformCondition(this.configuration.toString(), platform).toLowerCase()) { - outputPath = outputPathNodes[i].childNodes[0].nodeValue; - } else if (!condition) { - outputPath = outputPathNodes[i].childNodes[0].nodeValue; - } - } - } - - if (outputPath) { - this._outputPath = path.join(utils.getPlatformSafeFileEntryPath(this.projectFolderPath), utils.getPlatformSafeFileEntryPath(outputPath)); - } else { - // If output path isn't specified in .sqlproj, set it to the default output path .\bin\Debug\ - this._outputPath = path.join(utils.getPlatformSafeFileEntryPath(this.projectFolderPath), utils.getPlatformSafeFileEntryPath(constants.defaultOutputPath(this.configuration.toString()))); + for (const variable of sqlcmdVariablesResult.sqlCmdVariables) { + this._sqlCmdVariables[variable.varName] = variable.defaultValue; // store the default value that's specified in the .sqlproj } } @@ -248,639 +242,342 @@ export class Project implements ISqlProject { * Gets all the files specified by and removes all the files specified by * and all files included by the default glob of the folder of the sqlproj if it's an sdk style project */ - private async readFilesInProject(): Promise { + private async readFilesInProject(): Promise { const filesSet: Set = new Set(); - const entriesWithType: { relativePath: string, typeAttribute: string }[] = []; - // default glob include pattern for sdk style projects - if (this._isSdkStyleProject) { - try { - const globFiles = await utils.getSqlFilesInFolder(this.projectFolderPath, true); - globFiles.forEach(f => { - filesSet.add(utils.convertSlashesForSqlProj(utils.trimUri(Uri.file(this.projectFilePath), Uri.file(f)))); - }); - } catch (e) { - console.error(utils.getErrorMessage(e)); + var result: GetScriptsResult = await this.sqlProjService.getSqlObjectScripts(this.projectFilePath); + + this.throwIfFailed(result); + + if (result.scripts?.length > 0) { // empty array from SqlToolsService is deserialized as null + for (var script of result.scripts) { + filesSet.add(script); } } - for (let ig = 0; ig < this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ItemGroup).length; ig++) { - const itemGroup = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ItemGroup)[ig]; - - // find all files to include that are specified to be included and removed (for sdk style projects) in the project file - // the build elements are evaluated in the order they are in the sqlproj (same way sdk style csproj handles this) - try { - const buildElements = itemGroup.getElementsByTagName(constants.Build); - - for (let b = 0; b < buildElements.length; b++) { - // - const includeRelativePath = buildElements[b].getAttribute(constants.Include)!; - - if (includeRelativePath) { - const fullPath = path.join(utils.getPlatformSafeFileEntryPath(this.projectFolderPath), utils.getPlatformSafeFileEntryPath(includeRelativePath)); - - // sdk style projects can handle other globbing patterns like and - if (this._isSdkStyleProject && !(await utils.exists(fullPath))) { - // add files from the glob pattern - const globFiles = await utils.globWithPattern(fullPath); - globFiles.forEach(gf => { - const newFileRelativePath = utils.convertSlashesForSqlProj(utils.trimUri(Uri.file(this.projectFilePath), Uri.file(gf))); - filesSet.add(newFileRelativePath); - }); - } else { - filesSet.add(includeRelativePath); - - // Right now only used for external streaming jobs - const typeAttribute = buildElements[b].getAttribute(constants.Type)!; - if (typeAttribute) { - entriesWithType.push({ relativePath: includeRelativePath, typeAttribute: typeAttribute }); - } - } - } - - // - // remove files specified in the sqlproj to remove if this is an sdk style project - if (this._isSdkStyleProject) { - const removeRelativePath = buildElements[b].getAttribute(constants.Remove)!; - - if (removeRelativePath) { - const fullPath = path.join(utils.getPlatformSafeFileEntryPath(this.projectFolderPath), utils.getPlatformSafeFileEntryPath(removeRelativePath)); - - const globRemoveFiles = await utils.globWithPattern(fullPath); - globRemoveFiles.forEach(gf => { - const removeFileRelativePath = utils.convertSlashesForSqlProj(utils.trimUri(Uri.file(this.projectFilePath), Uri.file(gf))); - filesSet.delete(removeFileRelativePath); - }); - } - } - } - } catch (e) { - void window.showErrorMessage(constants.errorReadingProject(constants.BuildElements, this.projectFilePath)); - console.error(utils.getErrorMessage(e)); - } - } - - if (this.isSdkStyleProject) { - // remove any pre/post/none deploy scripts that were specified in the sqlproj so they aren't counted twice - this.preDeployScripts.forEach(f => filesSet.delete(f.relativePath)); - this.postDeployScripts.forEach(f => filesSet.delete(f.relativePath)); - this.noneDeployScripts.forEach(f => filesSet.delete(f.relativePath)); - - // remove any none remove scripts (these would be pre/post/none deploy scripts that were excluded) - const noneRemoveScripts = this.readNoneRemoveScripts(); - noneRemoveScripts.forEach(f => filesSet.delete(f.relativePath)); - } - // create a FileProjectEntry for each file const fileEntries: FileProjectEntry[] = []; for (let f of Array.from(filesSet.values())) { - const typeEntry = entriesWithType.find(e => e.relativePath === f); // read file to check if it has a "Create Table" statement const fullPath = path.join(utils.getPlatformSafeFileEntryPath(this.projectFolderPath), utils.getPlatformSafeFileEntryPath(f)); - const containsCreateTableStatement = await utils.fileContainsCreateTableStatement(fullPath, this.getProjectTargetVersion()); + const containsCreateTableStatement: boolean = await utils.fileContainsCreateTableStatement(fullPath, this.getProjectTargetVersion()); - fileEntries.push(this.createFileProjectEntry(f, EntryType.File, typeEntry ? typeEntry.typeAttribute : undefined, containsCreateTableStatement)); + fileEntries.push(this.createFileProjectEntry(f, EntryType.File, undefined, containsCreateTableStatement)); } - return fileEntries; + this._files = fileEntries; } - private async readFolders(): Promise { + private async readFolders(): Promise { + var result: GetFoldersResult = await this.sqlProjService.getFolders(this.projectFilePath); + this.throwIfFailed(result); + const folderEntries: FileProjectEntry[] = []; - const foldersSet = new Set(); - - // get any folders listed in the project file - const sqlprojFolders = await this.foldersListedInSqlproj(); - sqlprojFolders.forEach(f => foldersSet.add(f)); - - // glob style getting folders for sdk style projects - if (this._isSdkStyleProject) { - this.files.forEach(file => { - // if file is in the project's folder, add the folders from the project file to this file to the list of folders. This is so that only non-empty folders in the project folder will be added by default. - // Empty folders won't be shown unless specified in the sqlproj (same as how it's handled for csproj in VS) - if (!file.relativePath.startsWith('..') && path.dirname(file.fsUri.fsPath) !== this.projectFolderPath) { - const foldersToFile = utils.getFoldersToFile(this.projectFolderPath, file.fsUri.fsPath); - foldersToFile.forEach(f => foldersSet.add(utils.convertSlashesForSqlProj(utils.trimUri(Uri.file(this.projectFilePath), Uri.file(f))))); + if (result.folders?.length > 0) { // empty array from SqlToolsService is deserialized as null + for (var folderPath of result.folders) { + // Don't include folders that aren't supported: + // 1. Don't add Properties folder since it isn't supported in ADS.In SSDT, it isn't a physical folder, but it's specified in legacy sql projects + // to display the Properties node in the project tree. + // 2. Don't add external folders (relative path starts with "..") + if (folderPath === constants.Properties || folderPath.startsWith(constants.RelativeOuterPath)) { + continue; } - }); - // add any intermediate folders of the folders that are listed in the sqlproj - // If there are nested empty folders, there will only be a Folder entry for the inner most folder, so we need to add entries for the intermediate folders - sqlprojFolders.forEach(folder => { - const fullPath = path.join(utils.getPlatformSafeFileEntryPath(this.projectFolderPath), utils.getPlatformSafeFileEntryPath(folder)); - const intermediateFolders = utils.getFoldersAlongPath(this.projectFolderPath, utils.getPlatformSafeFileEntryPath(fullPath)); - intermediateFolders.forEach(f => foldersSet.add(utils.convertSlashesForSqlProj(utils.trimUri(Uri.file(this.projectFilePath), Uri.file(f))))); - }); - } - - foldersSet.forEach(f => { - folderEntries.push(this.createFileProjectEntry(f, EntryType.Folder)); - }); - - return folderEntries; - } - - /** - * @returns Array of folders specified in the sqlproj - */ - private async foldersListedInSqlproj(): Promise { - const folders: string[] = []; - - // get any folders listed in the project file - for (let ig = 0; ig < this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ItemGroup).length; ig++) { - const itemGroup = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ItemGroup)[ig]; - try { - const folderElements = itemGroup.getElementsByTagName(constants.Folder); - for (let f = 0; f < folderElements.length; f++) { - let relativePath = folderElements[f].getAttribute(constants.Include)!; - - // don't add Properties folder since it isn't supported for now and don't add if the folder was already added - if (utils.trimChars(relativePath, '\\') !== constants.Properties) { - // make sure folder relative path ends with \\ because sometimes SSDT adds folders without trailing \\ - folders.push(utils.ensureTrailingSlash(relativePath)); - } - } - } catch (e) { - void window.showErrorMessage(constants.errorReadingProject(constants.Folder, this.projectFilePath)); - console.error(utils.getErrorMessage(e)); + folderEntries.push(this.createFileProjectEntry(folderPath, EntryType.Folder)); } } - return folders; + this._folders = folderEntries; } - private readPreDeployScripts(): FileProjectEntry[] { - const preDeployScripts: FileProjectEntry[] = []; - // find all pre-deployment scripts to include - let preDeployScriptCount: number = 0; + private async readPreDeployScripts(warnIfMultiple: boolean = false): Promise { + var result: GetScriptsResult = await this.sqlProjService.getPreDeploymentScripts(this.projectFilePath); + this.throwIfFailed(result); - for (let ig = 0; ig < this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ItemGroup).length; ig++) { - const itemGroup = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ItemGroup)[ig]; + const preDeploymentScriptEntries: FileProjectEntry[] = []; - try { - const preDeploy = itemGroup.getElementsByTagName(constants.PreDeploy); - for (let pre = 0; pre < preDeploy.length; pre++) { - preDeployScripts.push(this.createFileProjectEntry(preDeploy[pre].getAttribute(constants.Include)!, EntryType.File)); - preDeployScriptCount++; - } - } catch (e) { - void window.showErrorMessage(constants.errorReadingProject(constants.PreDeployElements, this.projectFilePath)); - console.error(utils.getErrorMessage(e)); + if (result.scripts?.length > 0) { // empty array from SqlToolsService is deserialized as null + for (var scriptPath of result.scripts) { + preDeploymentScriptEntries.push(this.createFileProjectEntry(scriptPath, EntryType.File)); } } - if (preDeployScriptCount > 1) { + if (preDeploymentScriptEntries.length > 1 && warnIfMultiple) { void window.showWarningMessage(constants.prePostDeployCount, constants.okString); } - return preDeployScripts; + this._preDeployScripts = preDeploymentScriptEntries; } - private readPostDeployScripts(): FileProjectEntry[] { - const postDeployScripts: FileProjectEntry[] = []; - // find all post-deployment scripts to include - let postDeployScriptCount: number = 0; + private async readPostDeployScripts(warnIfMultiple: boolean = false): Promise { + var result: GetScriptsResult = await this.sqlProjService.getPostDeploymentScripts(this.projectFilePath); + this.throwIfFailed(result); - for (let ig = 0; ig < this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ItemGroup).length; ig++) { - const itemGroup = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ItemGroup)[ig]; + const postDeploymentScriptEntries: FileProjectEntry[] = []; - try { - const postDeploy = itemGroup.getElementsByTagName(constants.PostDeploy); - for (let post = 0; post < postDeploy.length; post++) { - postDeployScripts.push(this.createFileProjectEntry(postDeploy[post].getAttribute(constants.Include)!, EntryType.File)); - postDeployScriptCount++; - } - } catch (e) { - void window.showErrorMessage(constants.errorReadingProject(constants.PostDeployElements, this.projectFilePath)); - console.error(utils.getErrorMessage(e)); + if (result.scripts?.length > 0) { // empty array from SqlToolsService is deserialized as null + for (var scriptPath of result.scripts) { + postDeploymentScriptEntries.push(this.createFileProjectEntry(scriptPath, EntryType.File)); } } - if (postDeployScriptCount > 1) { + if (postDeploymentScriptEntries.length > 1 && warnIfMultiple) { void window.showWarningMessage(constants.prePostDeployCount, constants.okString); } - return postDeployScripts; + this._postDeployScripts = postDeploymentScriptEntries; } - private readNoneDeployScripts(): FileProjectEntry[] { - const noneDeployScripts: FileProjectEntry[] = []; + private async readNoneItems(): Promise { + var result: GetScriptsResult = await this.sqlProjService.getNoneItems(this.projectFilePath); + this.throwIfFailed(result); - for (let ig = 0; ig < this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ItemGroup).length; ig++) { - const itemGroup = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ItemGroup)[ig]; + const noneItemEntries: FileProjectEntry[] = []; - // find all none-deployment scripts to include - try { - const noneItems = itemGroup.getElementsByTagName(constants.None); - for (let n = 0; n < noneItems.length; n++) { - const includeAttribute = noneItems[n].getAttribute(constants.Include); - if (includeAttribute && !utils.isPublishProfile(includeAttribute)) { - noneDeployScripts.push(this.createFileProjectEntry(includeAttribute, EntryType.File)); - } - } - } catch (e) { - void window.showErrorMessage(constants.errorReadingProject(constants.NoneElements, this.projectFilePath)); - console.error(utils.getErrorMessage(e)); + if (result.scripts?.length > 0) { // empty array from SqlToolsService is deserialized as null + for (var path of result.scripts) { + noneItemEntries.push(this.createFileProjectEntry(path, EntryType.File)); } } - return noneDeployScripts; - } + this._noneDeployScripts = []; + this._publishProfiles = []; - /** - * @returns all the files specified as in the sqlproj - */ - private readNoneRemoveScripts(): FileProjectEntry[] { - const noneRemoveScripts: FileProjectEntry[] = []; - - for (let ig = 0; ig < this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ItemGroup).length; ig++) { - const itemGroup = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ItemGroup)[ig]; - - // find all none remove scripts to specified in the sqlproj - try { - const noneItems = itemGroup.getElementsByTagName(constants.None); - for (let n = 0; n < noneItems.length; n++) { - noneRemoveScripts.push(this.createFileProjectEntry(noneItems[n].getAttribute(constants.Remove)!, EntryType.File)); - } - } catch (e) { - void window.showErrorMessage(constants.errorReadingProject(constants.NoneElements, this.projectFilePath)); - console.error(utils.getErrorMessage(e)); + for (const entry of noneItemEntries) { + if (utils.isPublishProfile(entry.relativePath)) { + this._publishProfiles.push(entry); + } else { + this._noneDeployScripts.push(entry); } } - - return noneRemoveScripts; } - /** - * - * @returns all the publish profiles (ending with *.publish.xml) specified as in the sqlproj - */ - private readPublishProfiles(): FileProjectEntry[] { - const publishProfiles: FileProjectEntry[] = []; + private async readDatabaseReferences(): Promise { + this._databaseReferences = []; + const databaseReferencesResult = await this.sqlProjService.getDatabaseReferences(this.projectFilePath); - for (let ig = 0; ig < this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ItemGroup).length; ig++) { - const itemGroup = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ItemGroup)[ig]; + for (const dacpacReference of databaseReferencesResult.dacpacReferences) { + this._databaseReferences.push(new DacpacReferenceProjectEntry({ + dacpacFileLocation: Uri.file(dacpacReference.dacpacPath), + suppressMissingDependenciesErrors: dacpacReference.suppressMissingDependencies, - // find all publish profile scripts to include - try { - const noneItems = itemGroup.getElementsByTagName(constants.None); - for (let n = 0; n < noneItems.length; n++) { - const includeAttribute = noneItems[n].getAttribute(constants.Include); - if (includeAttribute && utils.isPublishProfile(includeAttribute)) { - publishProfiles.push(this.createFileProjectEntry(includeAttribute, EntryType.File)); - } - } - } catch (e) { - void window.showErrorMessage(constants.errorReadingProject(constants.PublishProfileElements, this.projectFilePath)); - console.error(utils.getErrorMessage(e)); - } + databaseVariableLiteralValue: dacpacReference.databaseVariableLiteralName, + databaseName: dacpacReference.databaseVariable?.varName, + databaseVariable: dacpacReference.databaseVariable?.value, + serverName: dacpacReference.serverVariable?.varName, + serverVariable: dacpacReference.serverVariable?.value + })); } - return publishProfiles; + for (const projectReference of databaseReferencesResult.sqlProjectReferences) { + this._databaseReferences.push(new SqlProjectReferenceProjectEntry({ + projectName: path.basename(utils.getPlatformSafeFileEntryPath(projectReference.projectPath), constants.sqlprojExtension), + projectGuid: projectReference.projectGuid ?? '', + suppressMissingDependenciesErrors: projectReference.suppressMissingDependencies, + projectRelativePath: Uri.file(utils.getPlatformSafeFileEntryPath(projectReference.projectPath)), + + databaseVariableLiteralValue: projectReference.databaseVariableLiteralName, + databaseName: projectReference.databaseVariable?.varName, + databaseVariable: projectReference.databaseVariable?.value, + serverName: projectReference.serverVariable?.varName, + serverVariable: projectReference.serverVariable?.value + })); + } + + for (const systemDbReference of databaseReferencesResult.systemDatabaseReferences) { + this._databaseReferences.push(new SystemDatabaseReferenceProjectEntry( + systemDbReference.systemDb === SystemDatabase.Master ? constants.master : constants.msdb, + systemDbReference.databaseVariableLiteralName, + systemDbReference.suppressMissingDependencies)); + } } - private readDatabaseReferences(): IDatabaseReferenceProjectEntry[] { - const databaseReferenceEntries: IDatabaseReferenceProjectEntry[] = []; - - // database(system db and dacpac) references - const references = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ArtifactReference); - for (let r = 0; r < references.length; r++) { - try { - if (references[r].getAttribute(constants.Condition) !== constants.NotNetCoreCondition) { - const filepath = references[r].getAttribute(constants.Include); - if (!filepath) { - throw new Error(constants.invalidDatabaseReference); - } - - const nameNodes = references[r].getElementsByTagName(constants.DatabaseVariableLiteralValue); - const name = nameNodes.length === 1 ? nameNodes[0].childNodes[0].nodeValue! : undefined; - - const suppressMissingDependenciesErrorNode = references[r].getElementsByTagName(constants.SuppressMissingDependenciesErrors); - const suppressMissingDependencies = suppressMissingDependenciesErrorNode.length === 1 ? (suppressMissingDependenciesErrorNode[0].childNodes[0].nodeValue === constants.True) : false; - - const path = utils.convertSlashesForSqlProj(this.getSystemDacpacUri(`${name}.dacpac`).fsPath); - if (path.includes(filepath)) { - databaseReferenceEntries.push(new SystemDatabaseReferenceProjectEntry( - Uri.file(filepath), - this.getSystemDacpacSsdtUri(`${name}.dacpac`), - name, - suppressMissingDependencies)); - } else { - databaseReferenceEntries.push(new DacpacReferenceProjectEntry({ - dacpacFileLocation: Uri.file(utils.getPlatformSafeFileEntryPath(filepath)), - databaseName: name, - suppressMissingDependenciesErrors: suppressMissingDependencies - })); - } - } - } catch (e) { - void window.showErrorMessage(constants.errorReadingProject(constants.DacpacReferenceElement, this.projectFilePath)); - console.error(utils.getErrorMessage(e)); - } - } - - // project references - const projectReferences = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ProjectReference); - for (let r = 0; r < projectReferences.length; r++) { - try { - const filepath = projectReferences[r].getAttribute(constants.Include); - if (!filepath) { - throw new Error(constants.invalidDatabaseReference); - } - - const nameNodes = projectReferences[r].getElementsByTagName(constants.Name); - let name = ''; - try { - name = nameNodes[0].childNodes[0].nodeValue!; - } catch (e) { - void window.showErrorMessage(constants.errorReadingProject(constants.ProjectReferenceNameElement, this.projectFilePath)); - console.error(utils.getErrorMessage(e)); - } - - const suppressMissingDependenciesErrorNode = projectReferences[r].getElementsByTagName(constants.SuppressMissingDependenciesErrors); - const suppressMissingDependencies = suppressMissingDependenciesErrorNode.length === 1 ? (suppressMissingDependenciesErrorNode[0].childNodes[0].nodeValue === constants.True) : false; - - databaseReferenceEntries.push(new SqlProjectReferenceProjectEntry({ - projectRelativePath: Uri.file(utils.getPlatformSafeFileEntryPath(filepath)), - projectName: name, - projectGuid: '', // don't care when just reading project as a reference - suppressMissingDependenciesErrors: suppressMissingDependencies - })); - } catch (e) { - void window.showErrorMessage(constants.errorReadingProject(constants.ProjectReferenceElement, this.projectFilePath)); - console.error(utils.getErrorMessage(e)); - } - } - - return databaseReferenceEntries; - } - - private readImportedTargets(): string[] { - const imports: string[] = []; - - // find all import statements to include - try { - const importElements = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.Import); - for (let i = 0; i < importElements.length; i++) { - const importTarget = importElements[i]; - imports.push(importTarget.getAttribute(constants.Project)!); - } - } catch (e) { - void window.showErrorMessage(constants.errorReadingProject(constants.ImportElements, this.projectFilePath)); - console.error(utils.getErrorMessage(e)); - } - - return imports; - } + //#endregion private resetProject(): void { this._files = []; - this._importedTargets = []; this._databaseReferences = []; this._sqlCmdVariables = {}; this._preDeployScripts = []; this._postDeployScripts = []; this._noneDeployScripts = []; - this.projFileXmlDoc = undefined; this._outputPath = ''; this._configuration = Configuration.Debug; } - /** - * Checks for the 3 possible ways a project can reference the sql project sdk - * https://docs.microsoft.com/en-us/visualstudio/msbuild/how-to-use-project-sdk?view=vs-2019 - * @returns true if the project is an sdk style project, false if it isn't - */ - public CheckForSdkStyleProject(): boolean { - // type 1: Sdk node like - const sdkNodes = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.Sdk); - if (sdkNodes.length > 0) { - return sdkNodes[0].getAttribute(constants.Name) === constants.sqlProjectSdk; - } - - // type 2: Project node has Sdk attribute like - const sdkAttribute: string = this.projFileXmlDoc!.documentElement.getAttribute(constants.Sdk)!; - if (sdkAttribute) { - return sdkAttribute.includes(constants.sqlProjectSdk); - } - - // type 3: Import node with Sdk attribute like - const importNodes = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.Import); - for (let i = 0; i < importNodes.length; i++) { - if (importNodes[i].getAttribute(constants.Sdk) === constants.sqlProjectSdk) { - return true; - } - } - - return false; - } - public async updateProjectForRoundTrip(): Promise { - if (this._importedTargets.includes(constants.NetCoreTargets) && !this.containsSSDTOnlySystemDatabaseReferences() // old style project check - || this.isSdkStyleProject) { // new style project check + if (this.isCrossPlatformCompatible) { return; } TelemetryReporter.sendActionEvent(TelemetryViews.ProjectController, TelemetryActions.updateProjectForRoundtrip); - if (!this._importedTargets.includes(constants.NetCoreTargets)) { - const result = await window.showWarningMessage(constants.updateProjectForRoundTrip(this.projectFileName), constants.yesString, constants.noString); - if (result === constants.yesString) { - await fs.copyFile(this._projectFilePath, this._projectFilePath + '_backup'); - await this.updateImportToSupportRoundTrip(); - await this.updatePackageReferenceInProjFile(); - await this.updateBeforeBuildTargetInProjFile(); - await this.updateSystemDatabaseReferencesInProjFile(); - } - } else if (this.containsSSDTOnlySystemDatabaseReferences()) { - const result = await window.showWarningMessage(constants.updateProjectDatabaseReferencesForRoundTrip(this.projectFileName), constants.yesString, constants.noString); - if (result === constants.yesString) { - await fs.copyFile(this._projectFilePath, this._projectFilePath + '_backup'); - await this.updateSystemDatabaseReferencesInProjFile(); - } - } + const result = await this.sqlProjService.updateProjectForCrossPlatform(this.projectFilePath); + this.throwIfFailed(result); + + await this.readCrossPlatformCompatibility(); } - private async updateImportToSupportRoundTrip(): Promise { - // update an SSDT project to include Net core target information - for (let i = 0; i < this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.Import).length; i++) { - const importTarget = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.Import)[i]; + //#region Add/Delete/Exclude functions - let condition = importTarget.getAttribute(constants.Condition); - let projectAttributeVal = importTarget.getAttribute(constants.Project); - - if (condition === constants.SqlDbPresentCondition && projectAttributeVal === constants.SqlDbTargets) { - await this.updateImportedTargetsToProjFile(constants.RoundTripSqlDbPresentCondition, projectAttributeVal, importTarget); - } - if (condition === constants.SqlDbNotPresentCondition && projectAttributeVal === constants.MsBuildtargets) { - await this.updateImportedTargetsToProjFile(constants.RoundTripSqlDbNotPresentCondition, projectAttributeVal, importTarget); - } - } - - await this.updateImportedTargetsToProjFile(constants.NetCoreCondition, constants.NetCoreTargets, undefined); - } - - private async updateBeforeBuildTargetInProjFile(): Promise { - // Search if clean target already present, update it - for (let i = 0; i < this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.Target).length; i++) { - const beforeBuildNode = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.Target)[i]; - const name = beforeBuildNode.getAttribute(constants.Name); - if (name === constants.BeforeBuildTarget) { - return await this.createCleanFileNode(beforeBuildNode); - } - } - - // If clean target not found, create new - const beforeBuildNode = this.projFileXmlDoc!.createElement(constants.Target); - beforeBuildNode.setAttribute(constants.Name, constants.BeforeBuildTarget); - this.projFileXmlDoc!.documentElement.appendChild(beforeBuildNode); - await this.createCleanFileNode(beforeBuildNode); - } - - public async convertProjectToSdkStyle(): Promise { - // don't do anything if the project is already SDK style or it's an SSDT project that hasn't been updated to build in ADS - if (this.isSdkStyleProject || !this._importedTargets.includes(constants.NetCoreTargets)) { - return false; - } - - // make backup copy of project - await fs.copyFile(this._projectFilePath, this._projectFilePath + '_backup'); - - try { - // remove Build includes and folder includes - const beforeFiles = this.files.filter(f => f.type === EntryType.File); - const beforeFolders = this.files.filter(f => f.type === EntryType.Folder); - - // remove Build includes - for (const file of beforeFiles) { - // only remove build includes in the same folder as the project - if (!file.relativePath.includes('..')) { - await this.exclude(file); - } - } - - // remove Folder includes - for (const folder of beforeFolders) { - await this.exclude(folder); - } - - // remove "Properties" folder if it's there. This isn't tracked in the project's folders here because ADS doesn't support it. - // It's a reserved folder only used for the UI in SSDT - try { - await this.removeFolderFromProjFile('Properties'); - } catch { } - - // remove SSDT and ADS SqlTasks imports - const importsToRemove = []; - for (let i = 0; i < this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.Import).length; i++) { - const importTarget = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.Import)[i]; - const projectAttributeVal = importTarget.getAttribute(constants.Project); - - if (projectAttributeVal === constants.NetCoreTargets || projectAttributeVal === constants.SqlDbTargets || projectAttributeVal === constants.MsBuildtargets) { - importsToRemove.push(importTarget); - } - } - - const importsParent = importsToRemove[0]?.parentNode; - importsToRemove.forEach(i => { - importsParent?.removeChild(i); - }); - - // remove VisualStudio properties - const vsPropsToRemove = []; - for (let i = 0; i < this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.VisualStudioVersion).length; i++) { - const visualStudioVersionNode = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.VisualStudioVersion)[i]; - const conditionAttributeVal = visualStudioVersionNode.getAttribute(constants.Condition); - - if (conditionAttributeVal === constants.VSVersionCondition || conditionAttributeVal === constants.SsdtExistsCondition) { - vsPropsToRemove.push(visualStudioVersionNode); - } - } - - for (let i = 0; i < this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.SSDTExists).length; i++) { - const ssdtExistsNode = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.SSDTExists)[i]; - const conditionAttributeVal = ssdtExistsNode.getAttribute(constants.Condition); - - if (conditionAttributeVal === constants.targetsExistsCondition) { - vsPropsToRemove.push(ssdtExistsNode); - } - } - - const vsPropsParent = vsPropsToRemove[0]?.parentNode; - vsPropsToRemove.forEach(i => { - vsPropsParent?.removeChild(i); - - // Remove the parent PropertyGroup if there aren't any other nodes. Only count element nodes, not text nodes - const otherChildren = Array.from(vsPropsParent!.childNodes).filter((c: ChildNode) => c.childNodes); - - if (otherChildren.length === 0) { - vsPropsParent!.parentNode?.removeChild(vsPropsParent!); - } - }); - - // add SDK node - const sdkNode = this.projFileXmlDoc!.createElement(constants.Sdk); - sdkNode.setAttribute(constants.Name, constants.sqlProjectSdk); - sdkNode.setAttribute(constants.Version, constants.sqlProjectSdkVersion); - - const projectNode = this.projFileXmlDoc!.documentElement; - projectNode.insertBefore(sdkNode, projectNode.firstChild); - - // TODO: also update system dacpac path, but might as well wait for them to get included in the SDK since the path will probably change again - - await this.serializeToProjFile(this.projFileXmlDoc!); - await this.readProjFile(); - - // Make sure the same files are included as before and there aren't extra files included by the default **/*.sql glob - for (const file of this.files.filter(f => f.type === EntryType.File)) { - if (!beforeFiles.find(f => f.pathForSqlProj() === file.pathForSqlProj())) { - await this.exclude(file); - } - } - - // add back any folders that were previously specified in the sqlproj, but aren't included by the **/*.sql glob because they're empty - const folders = this.files.filter(f => f.type === EntryType.Folder); - for (const folder of beforeFolders) { - if (!folders.find(f => f.relativePath === folder.relativePath)) { - await this.addFolderItem(folder.relativePath); - } - } - } catch (e) { - console.error(e); - - // if there was an uncaught error during conversion, rollback project update - await fs.copyFile(this._projectFilePath + '_backup', this._projectFilePath); - await this.readProjFile(); - return false; - } - - return true; - } - - private async createCleanFileNode(parentNode: Element): Promise { - const deleteFileNode = this.projFileXmlDoc!.createElement(constants.Delete); - deleteFileNode.setAttribute(constants.Files, constants.ProjJsonToClean); - parentNode.appendChild(deleteFileNode); - await this.serializeToProjFile(this.projFileXmlDoc!); - } + //#region Folders /** * Adds a folder to the project, and saves the project file - * * @param relativeFolderPath Relative path of the folder */ - public async addFolderItem(relativeFolderPath: string): Promise { - const folderEntry = await this.ensureFolderItems(relativeFolderPath); + public async addFolder(relativeFolderPath: string): Promise { + if (relativeFolderPath.endsWith('\\')) { + relativeFolderPath = relativeFolderPath.slice(0, -1); + } - if (folderEntry) { - return folderEntry; - } else { - throw new Error(constants.outsideFolderPath); + const result = await this.sqlProjService.addFolder(this.projectFilePath, relativeFolderPath); + this.throwIfFailed(result); + + await this.readFolders(); + } + + public async deleteFolder(relativeFolderPath: string): Promise { + const result = await this.sqlProjService.deleteFolder(this.projectFilePath, relativeFolderPath); + this.throwIfFailed(result); + + await this.readFolders(); + } + + //#endregion + + //#region SQL object scripts + + public async addSqlObjectScript(relativePath: string): Promise { + const result = await this.sqlProjService.addSqlObjectScript(this.projectFilePath, relativePath); + this.throwIfFailed(result); + + await this.readFilesInProject(); + await this.readFolders(); + } + + public async addSqlObjectScripts(relativePaths: string[]): Promise { + for (const path of relativePaths) { + await this.addSqlObjectScript(path); } } + public async deleteSqlObjectScript(relativePath: string): Promise { + const result = await this.sqlProjService.deleteSqlObjectScript(this.projectFilePath, relativePath); + this.throwIfFailed(result); + + await this.readFilesInProject(); + await this.readFolders(); + } + + public async excludeSqlObjectScript(relativePath: string): Promise { + const result = await this.sqlProjService.excludeSqlObjectScript(this.projectFilePath, relativePath); + this.throwIfFailed(result); + + await this.readFilesInProject(); + await this.readFolders(); + } + + //#endregion + + //#region Pre-deployment scripts + + public async addPreDeploymentScript(relativePath: string): Promise { + if (this.preDeployScripts.length > 0) { + void vscode.window.showInformationMessage(constants.deployScriptExists(constants.PreDeploy)); + } + + const result = await this.sqlProjService.addPreDeploymentScript(this.projectFilePath, relativePath); + this.throwIfFailed(result); + + await this.readPreDeployScripts(); + await this.readNoneItems(); + await this.readFolders(); + } + + public async deletePreDeploymentScript(relativePath: string): Promise { + const result = await this.sqlProjService.deletePreDeploymentScript(this.projectFilePath, relativePath); + this.throwIfFailed(result); + + await this.readPreDeployScripts(); + await this.readFolders(); + } + + public async excludePreDeploymentScript(relativePath: string): Promise { + const result = await this.sqlProjService.excludePreDeploymentScript(this.projectFilePath, relativePath); + this.throwIfFailed(result); + + await this.readPreDeployScripts(); + await this.readFolders(); + } + + //#endregion + + //#region Post-deployment scripts + + public async addPostDeploymentScript(relativePath: string): Promise { + if (this.postDeployScripts.length > 0) { + void vscode.window.showInformationMessage(constants.deployScriptExists(constants.PostDeploy)); + } + + const result = await this.sqlProjService.addPostDeploymentScript(this.projectFilePath, relativePath); + this.throwIfFailed(result); + + await this.readPostDeployScripts(); + await this.readNoneItems(); + await this.readFolders(); + } + + public async deletePostDeploymentScript(relativePath: string): Promise { + const result = await this.sqlProjService.deletePostDeploymentScript(this.projectFilePath, relativePath); + this.throwIfFailed(result); + + await this.readPostDeployScripts(); + await this.readFolders(); + } + + public async excludePostDeploymentScript(relativePath: string): Promise { + const result = await this.sqlProjService.excludePostDeploymentScript(this.projectFilePath, relativePath); + this.throwIfFailed(result); + + await this.readPostDeployScripts(); + await this.readFolders(); + } + + //#endregion + + //#region None items + + public async addNoneItem(relativePath: string): Promise { + const result = await this.sqlProjService.addNoneItem(this.projectFilePath, relativePath); + this.throwIfFailed(result); + + await this.readNoneItems(); + await this.readFolders(); + } + + public async deleteNoneItem(relativePath: string): Promise { + const result = await this.sqlProjService.deleteNoneItem(this.projectFilePath, relativePath); + this.throwIfFailed(result); + + await this.readNoneItems(); + await this.readFolders(); + } + + public async excludeNoneItem(relativePath: string): Promise { + const result = await this.sqlProjService.excludeNoneItem(this.projectFilePath, relativePath); + this.throwIfFailed(result); + + await this.readNoneItems(); + await this.readFolders(); + } + + //#endregion + + //#endregion + /** * Writes a file to disk if contents are provided, adds that file to the project, and writes it to disk * @@ -889,33 +586,6 @@ export class Project implements ISqlProject { * @param itemType Type of the project entry to add. This maps to the build action for the item. */ public async addScriptItem(relativeFilePath: string, contents?: string, itemType?: string): Promise { - const absoluteFilePath = path.join(this.projectFolderPath, relativeFilePath); - - if (contents) { - // Create the file if contents were passed in and file does not exist yet - await fs.mkdir(path.dirname(absoluteFilePath), { recursive: true }); - - try { - await fs.writeFile(absoluteFilePath, contents, { flag: 'wx' }); - } catch (error) { - if (error.code === 'EEXIST') { - // Throw specialized error, if file already exists - throw new Error(constants.fileAlreadyExists(path.parse(absoluteFilePath).name)); - } - - throw error; - } - } else { - // If no contents were provided, then check that file already exists - let exists = await utils.exists(absoluteFilePath); - if (!exists) { - throw new Error(constants.noFileExist(absoluteFilePath)); - } - } - - // Ensure that parent folder item exist in the project for the corresponding file path - await this.ensureFolderItems(path.relative(this.projectFolderPath, path.dirname(absoluteFilePath))); - // Check if file already has been added to sqlproj const normalizedRelativeFilePath = utils.convertSlashesForSqlProj(relativeFilePath); @@ -924,34 +594,23 @@ export class Project implements ISqlProject { return existingEntry; } - // Update sqlproj XML - const fileEntry = this.createFileProjectEntry(normalizedRelativeFilePath, EntryType.File); + // Ensure the file exists // TODO: can be pushed down to DacFx + const absoluteFilePath = path.join(this.projectFolderPath, relativeFilePath); + await utils.ensureFileExists(absoluteFilePath, contents); - let xmlTag; switch (itemType) { case ItemType.preDeployScript: - xmlTag = constants.PreDeploy; - this._preDeployScripts.length === 0 ? this._preDeployScripts.push(fileEntry) : this._noneDeployScripts.push(fileEntry); + await this.addPreDeploymentScript(relativeFilePath); break; case ItemType.postDeployScript: - xmlTag = constants.PostDeploy; - this._postDeployScripts.length === 0 ? this._postDeployScripts.push(fileEntry) : this._noneDeployScripts.push(fileEntry); + await this.addPostDeploymentScript(relativeFilePath); break; default: - xmlTag = constants.Build; - this._files.push(fileEntry); + await this.addSqlObjectScript(relativeFilePath); + break; } - const attributes = new Map(); - - if (itemType === ItemType.externalStreamingJob) { - fileEntry.sqlObjectType = constants.ExternalStreamingJob; - attributes.set(constants.Type, constants.ExternalStreamingJob); - } - - await this.addToProjFile(fileEntry, xmlTag, attributes); - - return fileEntry; + return this.createFileProjectEntry(normalizedRelativeFilePath, EntryType.File); } /** @@ -965,50 +624,21 @@ export class Project implements ISqlProject { throw new Error(constants.noFileExist(filePath)); } - // Check if file already has been added to sqlproj const normalizedRelativeFilePath = utils.convertSlashesForSqlProj(path.relative(this.projectFolderPath, filePath)); - const existingEntry = this.files.find(f => f.relativePath.toUpperCase() === normalizedRelativeFilePath.toUpperCase()); - if (existingEntry) { - return existingEntry; + let result: ResultStatus; + + if (path.extname(filePath) === constants.sqlFileExtension) { + result = await this.sqlProjService.addSqlObjectScript(this.projectFilePath, normalizedRelativeFilePath) + await this.readFilesInProject(); + } else { + result = await this.sqlProjService.addNoneItem(this.projectFilePath, normalizedRelativeFilePath); + await this.readNoneItems(); } - // Ensure that parent folder item exist in the project for the corresponding file path - await this.ensureFolderItems(path.relative(this.projectFolderPath, path.dirname(filePath))); + this.throwIfFailed(result); + await this.readFolders(); - // Update sqlproj XML - const fileEntry = this.createFileProjectEntry(normalizedRelativeFilePath, EntryType.File); - const xmlTag = path.extname(filePath) === constants.sqlFileExtension ? constants.Build : constants.None; - await this.addToProjFile(fileEntry, xmlTag); - this._files.push(fileEntry); - - return fileEntry; - } - - public async exclude(entry: FileProjectEntry): Promise { - const toExclude: FileProjectEntry[] = this._files.concat(this._preDeployScripts).concat(this._postDeployScripts).concat(this._noneDeployScripts).concat(this._publishProfiles).filter(x => x.fsUri.fsPath.startsWith(entry.fsUri.fsPath)); - await this.removeFromProjFile(toExclude); - - this._files = this._files.filter(x => !x.fsUri.fsPath.startsWith(entry.fsUri.fsPath)); - this._preDeployScripts = this._preDeployScripts.filter(x => !x.fsUri.fsPath.startsWith(entry.fsUri.fsPath)); - this._postDeployScripts = this._postDeployScripts.filter(x => !x.fsUri.fsPath.startsWith(entry.fsUri.fsPath)); - this._noneDeployScripts = this._noneDeployScripts.filter(x => !x.fsUri.fsPath.startsWith(entry.fsUri.fsPath)); - this._publishProfiles = this._publishProfiles.filter(x => !x.fsUri.fsPath.startsWith(entry.fsUri.fsPath)); - } - - public async deleteFileFolder(entry: FileProjectEntry): Promise { - // compile a list of folder contents to delete; if entry is a file, contents will contain only itself - const toDeleteFiles: FileProjectEntry[] = this._files.concat(this._preDeployScripts).concat(this._postDeployScripts).concat(this._noneDeployScripts).concat(this._publishProfiles).filter(x => x.fsUri.fsPath.startsWith(entry.fsUri.fsPath) && x.type === EntryType.File); - const toDeleteFolders: FileProjectEntry[] = this._files.filter(x => x.fsUri.fsPath.startsWith(entry.fsUri.fsPath) && x.type === EntryType.Folder); - - await Promise.all(toDeleteFiles.map(x => fs.unlink(x.fsUri.fsPath))); - await Promise.all(toDeleteFolders.map(x => fs.rm(x.fsUri.fsPath, { recursive: true, force: true }))); - - await this.exclude(entry); - } - - public async deleteDatabaseReference(entry: IDatabaseReferenceProjectEntry): Promise { - await this.removeFromProjFile(entry); - this._databaseReferences = this._databaseReferences.filter(x => x !== entry); + return this.createFileProjectEntry(normalizedRelativeFilePath, EntryType.File); } /** @@ -1016,107 +646,32 @@ export class Project implements ISqlProject { * @param compatLevel compat level of project */ public async changeTargetPlatform(compatLevel: string): Promise { - if (this.getProjectTargetVersion() !== compatLevel) { - TelemetryReporter.createActionEvent(TelemetryViews.ProjectTree, TelemetryActions.changePlatformType) - .withAdditionalProperties({ - from: this.getProjectTargetVersion(), - to: compatLevel - }) - .send(); - - const newDSP = `${constants.MicrosoftDatatoolsSchemaSqlSql}${compatLevel}${constants.databaseSchemaProvider}`; - (this.projFileXmlDoc!.getElementsByTagName(constants.DSP)[0].childNodes[0] as Text).data = newDSP; - this.projFileXmlDoc!.getElementsByTagName(constants.DSP)[0].childNodes[0].nodeValue = newDSP; - - // update any system db references - const systemDbReferences = this._databaseReferences.filter(r => r instanceof SystemDatabaseReferenceProjectEntry) as SystemDatabaseReferenceProjectEntry[]; - if (systemDbReferences.length > 0) { - for (let r of systemDbReferences) { - // remove old entry in sqlproj - this.removeDatabaseReferenceFromProjFile(r); - - // update uris to point to the correct dacpacs for the target platform - r.fsUri = this.getSystemDacpacUri(`${r.databaseName}.dacpac`); - r.ssdtUri = this.getSystemDacpacSsdtUri(`${r.databaseName}.dacpac`); - - // add updated system db reference to sqlproj - await this.addDatabaseReferenceToProjFile(r); - } - } - - await this.serializeToProjFile(this.projFileXmlDoc!); - } - } - - /** - * Adds reference to the appropriate system database dacpac to the project - */ - public async addSystemDatabaseReference(settings: ISystemDatabaseReferenceSettings): Promise { - let uri: Uri; - let ssdtUri: Uri; - - if (settings.systemDb === SystemDatabase.master) { - uri = this.getSystemDacpacUri(constants.masterDacpac); - ssdtUri = this.getSystemDacpacSsdtUri(constants.masterDacpac); - } else { - uri = this.getSystemDacpacUri(constants.msdbDacpac); - ssdtUri = this.getSystemDacpacSsdtUri(constants.msdbDacpac); + if (this.getProjectTargetVersion() === compatLevel) { + return; } - const systemDatabaseReferenceProjectEntry = new SystemDatabaseReferenceProjectEntry(uri, ssdtUri, settings.databaseName, settings.suppressMissingDependenciesErrors); + TelemetryReporter.createActionEvent(TelemetryViews.ProjectTree, TelemetryActions.changePlatformType) + .withAdditionalProperties({ + from: this.getProjectTargetVersion(), + to: compatLevel + }) + .send(); - // check if reference to this database already exists - if (this.databaseReferenceExists(systemDatabaseReferenceProjectEntry)) { - throw new Error(constants.databaseReferenceAlreadyExists); - } - - await this.addToProjFile(systemDatabaseReferenceProjectEntry); - } - - public getSystemDacpacUri(dacpac: string): Uri { - const versionFolder = this.getSystemDacpacFolderName(); - const systemDacpacLocation = this.isSdkStyleProject ? '$(SystemDacpacsLocation)' : '$(NETCoreTargetsPath)'; - return Uri.parse(path.join(systemDacpacLocation, 'SystemDacpacs', versionFolder, dacpac)); - } - - public getSystemDacpacSsdtUri(dacpac: string): Uri { - const versionFolder = this.getSystemDacpacFolderName(); - return Uri.parse(path.join('$(DacPacRootPath)', 'Extensions', 'Microsoft', 'SQLDB', 'Extensions', 'SqlServer', versionFolder, 'SqlSchemas', dacpac)); - } - - public getSystemDacpacFolderName(): string { - const version = this.getProjectTargetVersion(); - - // DW is special because the target version is DW, but the folder name for system dacpacs is AzureDW in SSDT - // the other target versions have the same version name and folder name - return version === constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlDW) ? constants.AzureDwFolder : version; + this._databaseSchemaProvider = `${constants.MicrosoftDatatoolsSchemaSqlSql}${compatLevel}${constants.databaseSchemaProvider}`; + const result = await this.sqlProjService.setDatabaseSchemaProvider(this.projectFilePath, this._databaseSchemaProvider); + this.throwIfFailed(result); } /** * Gets the project target version specified in the DSP property in the sqlproj */ public getProjectTargetVersion(): string { - let dsp: string | undefined; - - try { - dsp = this.evaluateProjectPropertyValue(constants.DSP); - } - catch { - // We will throw specialized error instead - } - - // Check if DSP is missing or invalid - if (!dsp) { - throw new Error(constants.invalidDataSchemaProvider); - } - - // get version from dsp, which is a string like Microsoft.Data.Tools.Schema.Sql.Sql130DatabaseSchemaProvider - // Remove prefix and suffix to only get the actual version number/name. For the example above the result - // should be just '130'. + // Get version from dsp, which is a string like "Microsoft.Data.Tools.Schema.Sql.Sql130DatabaseSchemaProvider" + // Remove prefix and suffix to only get the actual version number/name. For the example above, the result should be just '130'. const version = - dsp.substring( + this._databaseSchemaProvider.substring( constants.MicrosoftDatatoolsSchemaSqlSql.length, - dsp.length - constants.databaseSchemaProvider.length); + this._databaseSchemaProvider.length - constants.databaseSchemaProvider.length); // make sure version is valid if (!Array.from(constants.targetPlatformToVersion.values()).includes(version)) { @@ -1132,7 +687,28 @@ export class Project implements ISqlProject { * @returns Default collation for the database set in the project. */ public getDatabaseDefaultCollation(): string { - return this.evaluateProjectPropertyValue(constants.DefaultCollationProperty, constants.DefaultCollation); + return this._defaultCollation; + } + + //#region Database References + + /** + * Adds reference to the appropriate system database dacpac to the project + */ + public async addSystemDatabaseReference(settings: ISystemDatabaseReferenceSettings): Promise { + // check if reference to this database already exists + if (this.databaseReferences.find(r => r.referenceName === settings.databaseVariableLiteralValue)) { + throw new Error(constants.databaseReferenceAlreadyExists); + } + + const systemDb = settings.systemDb as SystemDatabase; + const result = await this.sqlProjService.addSystemDatabaseReference(this.projectFilePath, systemDb, settings.suppressMissingDependenciesErrors, settings.databaseVariableLiteralValue); + + if (!result.success && result.errorMessage) { + throw new Error(constants.errorAddingDatabaseReference(utils.systemDatabaseToString(settings.systemDb), result.errorMessage)); + } + + await this.readDatabaseReferences(); } /** @@ -1140,13 +716,7 @@ export class Project implements ISqlProject { */ public async addDatabaseReference(settings: IDacpacReferenceSettings): Promise { const databaseReferenceEntry = new DacpacReferenceProjectEntry(settings); - - // check if reference to this database already exists - if (this.databaseReferenceExists(databaseReferenceEntry)) { - throw new Error(constants.databaseReferenceAlreadyExists); - } - - await this.addToProjFile(databaseReferenceEntry); + await this.addUserDatabaseReference(settings, databaseReferenceEntry); } /** @@ -1154,33 +724,116 @@ export class Project implements ISqlProject { */ public async addProjectReference(settings: IProjectReferenceSettings): Promise { const projectReferenceEntry = new SqlProjectReferenceProjectEntry(settings); + await this.addUserDatabaseReference(settings, projectReferenceEntry); + } + private async addUserDatabaseReference(settings: IProjectReferenceSettings | IDacpacReferenceSettings, reference: SqlProjectReferenceProjectEntry | DacpacReferenceProjectEntry): Promise { // check if reference to this database already exists - if (this.databaseReferenceExists(projectReferenceEntry)) { + if (this.databaseReferenceExists(reference)) { throw new Error(constants.databaseReferenceAlreadyExists); } - await this.addToProjFile(projectReferenceEntry); + // create database variable + if (settings.databaseVariable && settings.databaseName) { + await this.sqlProjService.addSqlCmdVariable(this.projectFilePath, settings.databaseVariable, settings.databaseName); + + // create server variable - only can be set when there's also a database variable (reference to different database on different server) + if (settings.serverVariable && settings.serverName) { + await this.sqlProjService.addSqlCmdVariable(this.projectFilePath, settings.serverVariable, settings.serverName); + } + + await this.readSqlCmdVariables(); + } + + const databaseLiteral = settings.databaseVariable ? undefined : settings.databaseName; + + let result; + let referenceName; + if (reference instanceof SqlProjectReferenceProjectEntry) { + referenceName = (settings).projectName; + result = await this.sqlProjService.addSqlProjectReference(this.projectFilePath, reference.pathForSqlProj(), reference.projectGuid, settings.suppressMissingDependenciesErrors, settings.databaseVariable, settings.serverVariable, databaseLiteral) + } else { // dacpac + referenceName = (settings).dacpacFileLocation.fsPath; + result = await this.sqlProjService.addDacpacReference(this.projectFilePath, reference.pathForSqlProj(), settings.suppressMissingDependenciesErrors, settings.databaseVariable, settings.serverVariable, databaseLiteral) + } + + if (!result.success && result.errorMessage) { + throw new Error(constants.errorAddingDatabaseReference(referenceName, result.errorMessage)); + } + + await this.readDatabaseReferences(); } + private databaseReferenceExists(entry: IDatabaseReferenceProjectEntry): boolean { + const found = this._databaseReferences.find(reference => reference.pathForSqlProj() === entry.pathForSqlProj()) !== undefined; + return found; + } + + public async deleteDatabaseReferenceByEntry(entry: IDatabaseReferenceProjectEntry): Promise { + await this.deleteDatabaseReference(entry.pathForSqlProj()); + } + + public async deleteDatabaseReference(name: string): Promise { + const result = await this.sqlProjService.deleteDatabaseReference(this.projectFilePath, name); + this.throwIfFailed(result); + await this.readDatabaseReferences(); + } + + //#endregion + + //#region SQLCMD Variables + /** * Adds a SQLCMD variable to the project * @param name name of the variable * @param defaultValue */ public async addSqlCmdVariable(name: string, defaultValue: string): Promise { - const sqlCmdVariableEntry = new SqlCmdVariableProjectEntry(name, defaultValue); - await this.addToProjFile(sqlCmdVariableEntry); + await this.sqlProjService.addSqlCmdVariable(this.projectFilePath, name, defaultValue); + await this.readSqlCmdVariables(); } + /** + * Updates a SQLCMD variable in the project + * @param name name of the variable + * @param defaultValue + */ + public async updateSqlCmdVariable(name: string, defaultValue: string): Promise { + await this.sqlProjService.updateSqlCmdVariable(this.projectFilePath, name, defaultValue); + await this.readSqlCmdVariables(); + } + + public async deleteSqlCmdVariable(variableName: string): Promise { + const result = await this.sqlProjService.deleteSqlCmdVariable(this.projectFilePath, variableName); + this.throwIfFailed(result); + await this.readSqlCmdVariables(); + } + + //#endregion + /** * Appends given database source to the DatabaseSource property element. * If property element does not exist, then new one will be created. * * @param databaseSource Source of the database to add */ - public addDatabaseSource(databaseSource: string): Promise { - return this.addValueToCollectionProjectProperty(constants.DatabaseSource, databaseSource); + public async addDatabaseSource(databaseSource: string): Promise { + if (databaseSource.includes(';')) { + throw Error(constants.invalidProjectPropertyValueProvided(';')); + } + + const sources: string[] = this.getDatabaseSourceValues(); + const index = sources.findIndex(x => x === databaseSource); + + if (index !== -1) { + return; + } + + sources.push(databaseSource); + const result = await this.sqlProjService.setDatabaseSource(this.projectFilePath, sources.join(';')); + this.throwIfFailed(result); + + await this.readProjectProperties(); } /** @@ -1189,8 +842,24 @@ export class Project implements ISqlProject { * * @param databaseSource Source of the database to remove */ - public removeDatabaseSource(databaseSource: string): Promise { - return this.removeValueFromCollectionProjectProperty(constants.DatabaseSource, databaseSource); + public async removeDatabaseSource(databaseSource: string): Promise { + if (databaseSource.includes(';')) { + throw Error(constants.invalidProjectPropertyValueProvided(';')); + } + + const sources: string[] = this.getDatabaseSourceValues(); + const index = sources.findIndex(x => x === databaseSource); + + if (index === -1) { + return; + } + + sources.splice(index, 1); + + const result = await this.sqlProjService.setDatabaseSource(this.projectFilePath, sources.join(';')); + this.throwIfFailed(result); + + await this.readProjectProperties(); } /** @@ -1199,25 +868,7 @@ export class Project implements ISqlProject { * @returns Array of all database sources */ public getDatabaseSourceValues(): string[] { - return this.getCollectionProjectPropertyValue(constants.DatabaseSource); - } - - /** - * Adds publish profile to the project - * - * @param relativeFilePath Relative path of the file - */ - public async addPublishProfileToProjFile(absolutePublishProfilePath: string): Promise { - const relativePublishProfilePath = (utils.trimUri(Uri.file(this.projectFilePath), Uri.file(absolutePublishProfilePath))); - - // Update sqlproj XML - - const fileEntry = this.createFileProjectEntry(relativePublishProfilePath, EntryType.File); - this._publishProfiles.push(fileEntry); - - await this.addToProjFile(fileEntry, constants.None); - - return fileEntry; + return this._databaseSource.trim() === '' ? [] : this._databaseSource.split(';'); } public createFileProjectEntry(relativePath: string, entryType: EntryType, sqlObjectType?: string, containsCreateTableStatement?: boolean): FileProjectEntry { @@ -1230,916 +881,42 @@ export class Project implements ISqlProject { containsCreateTableStatement); } - private findOrCreateItemGroup(containedTag?: string, prePostScriptExist?: { scriptExist: boolean; }): Element { - let outputItemGroup: Element[] = []; // "None" can have more than one ItemGroup, for "None Include" (for pre/post deploy scripts and publish profiles), "None Remove" - let returnItemGroup; - - // search for a particular item goup if a child type is provided - if (containedTag) { - // find any ItemGroup node that contains files; that's where we'll add - for (let ig = 0; ig < this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ItemGroup).length; ig++) { - const currentItemGroup = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ItemGroup)[ig]; - - if (currentItemGroup.getElementsByTagName(containedTag).length > 0) { - outputItemGroup.push(currentItemGroup); - } - } + private throwIfFailed(result: ResultStatus): void { + if (!result.success) { + throw new Error(constants.errorPrefix(result.errorMessage)); } - - // if none already exist, make a new ItemGroup for it - if (outputItemGroup.length === 0) { - returnItemGroup = this.projFileXmlDoc!.createElement(constants.ItemGroup); - this.projFileXmlDoc!.documentElement.appendChild(returnItemGroup); - - if (prePostScriptExist) { - prePostScriptExist.scriptExist = false; - } - } else { // if item group exists and containedTag = None, read the content to find None Include with publish profile - if (containedTag === constants.None) { - for (let ig = 0; ig < outputItemGroup.length; ig++) { - const itemGroup = outputItemGroup[ig]; - - // find all none include scripts specified in the sqlproj - const noneItems = itemGroup.getElementsByTagName(constants.None); - for (let n = 0; n < noneItems.length; n++) { - let noneIncludeItem = noneItems[n].getAttribute(constants.Include); - if (noneIncludeItem && utils.isPublishProfile(noneIncludeItem)) { - returnItemGroup = itemGroup; - break; - } - } - } - if (!returnItemGroup) { - returnItemGroup = this.projFileXmlDoc!.createElement(constants.ItemGroup); - this.projFileXmlDoc!.documentElement.appendChild(returnItemGroup); - } - } else { - returnItemGroup = outputItemGroup[0]; // Return the first item group that was found, to match prior implementation - } - } - - return returnItemGroup; - } - - private async addFileToProjFile(filePath: string, xmlTag: string, attributes?: Map): Promise { - - // delete Remove node if a file has been previously excluded - await this.undoExcludeFileFromProjFile(xmlTag, filePath); - - let itemGroup; - - if (xmlTag === constants.PreDeploy || xmlTag === constants.PostDeploy) { - let prePostScriptExist = { scriptExist: true }; - itemGroup = this.findOrCreateItemGroup(xmlTag, prePostScriptExist); - - if (prePostScriptExist.scriptExist === true) { - void window.showInformationMessage(constants.deployScriptExists(xmlTag)); - xmlTag = constants.None; // Add only one pre-deploy and post-deploy script. All additional ones get added in the same item group with None tag - } - } else if (xmlTag === constants.None) { // Add publish profiles with None tag - itemGroup = this.findOrCreateItemGroup(xmlTag); - } - else { - if (this.isSdkStyleProject) { - // if there's a folder entry for the folder containing this file, remove it from the sqlproj because the folder will now be - // included by the glob that includes this file (same as how csproj does it) - const folders = await this.foldersListedInSqlproj(); - folders.forEach(folder => { - const trimmedUri = utils.trimUri(Uri.file(utils.getPlatformSafeFileEntryPath(folder)), Uri.file(utils.getPlatformSafeFileEntryPath(filePath))); - const basename = path.basename(utils.getPlatformSafeFileEntryPath(filePath)); - if (trimmedUri === basename) { - // remove folder entry from sqlproj - this.removeFolderNode(folder); - } - }); - } - - const currentFiles = await this.readFilesInProject(); - - // don't need to add an entry if it's already included by a glob pattern - // unless it has an attribute that needs to be added, like external streaming job which needs it so it can be determined if validation can run on it - if ((!attributes || attributes.size === 0) && currentFiles.find(f => f.relativePath === utils.convertSlashesForSqlProj(filePath))) { - return; - } - - itemGroup = this.findOrCreateItemGroup(xmlTag); - } - - const newFileNode = this.projFileXmlDoc!.createElement(xmlTag); - - newFileNode.setAttribute(constants.Include, utils.convertSlashesForSqlProj(filePath)); - - if (attributes) { - for (const key of attributes.keys()) { - newFileNode.setAttribute(key, attributes.get(key)!); - } - } - - itemGroup.appendChild(newFileNode); - } - - private async removeFileFromProjFile(path: string): Promise {//TODO: publish profile - const fileNodes = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.Build); - const preDeployNodes = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.PreDeploy); - const postDeployNodes = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.PostDeploy); - const noneNodes = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.None); - const nodes = [fileNodes, preDeployNodes, postDeployNodes, noneNodes]; - - const isBuildElement = this.files.find(f => f.relativePath === path); - - let deleted = false; - - // remove the entry if there is one - for (let i = 0; i < nodes.length; i++) { - deleted = this.removeNode(path, nodes[i]); - - if (deleted) { - // still might need to add a node if this is an sdk style project - if (this.isSdkStyleProject) { - break; - } else { - return; - } - } - } - - // if it's an sdk style project, we'll need to add a entry to remove this file if it's - // still included by a glob - if (this.isSdkStyleProject) { - // write any changes from removing an include node and get the current files included in the project - if (deleted) { - await this.serializeToProjFile(this.projFileXmlDoc!); - } - this._preDeployScripts = this.readPreDeployScripts(); - this._postDeployScripts = this.readPostDeployScripts(); - this._noneDeployScripts = this.readNoneDeployScripts(); - const currentFiles = await this.readFilesInProject(); - - // only add a Remove node to exclude the file if it's still included by a glob - if (currentFiles.find(f => f.relativePath === utils.convertSlashesForSqlProj(path))) { - const removeFileNode = isBuildElement ? this.projFileXmlDoc!.createElement(constants.Build) : this.projFileXmlDoc!.createElement(constants.None); - removeFileNode.setAttribute(constants.Remove, utils.convertSlashesForSqlProj(path)); - this.findOrCreateItemGroup(constants.Build).appendChild(removeFileNode); - return; - } - - return; - } - - throw new Error(constants.unableToFindObject(path, constants.fileObject)); } /** - * Deletes a node from the project file similar to - * @param includeString Path of the file that matches the Include portion of the node - * @param nodes The collection of XML nodes to search from - * @param undoRemove When true, will remove a node similar to - * @returns True when a node has been removed, false otherwise. + * Moves a file to a different location + * @param node Node being moved + * @param projectFilePath Full file path to .sqlproj + * @param destinationRelativePath path of the destination, relative to .sqlproj */ - private removeNode(includeString: string, nodes: HTMLCollectionOf, undoRemove: boolean = false): boolean { - // Default function behavior removes nodes like - // However when undoRemove is true, this function removes - const xmlAttribute = undoRemove ? constants.Remove : constants.Include; - for (let i = 0; i < nodes.length; i++) { - const parent = nodes[i].parentNode; + public async move(node: BaseProjectTreeItem, destinationRelativePath: string): Promise { + // trim off the project folder at the beginning of the relative path stored in the tree + const projectRelativeUri = vscode.Uri.file(path.basename(this.projectFilePath, constants.sqlprojExtension)); + const originalRelativePath = utils.trimUri(projectRelativeUri, node.relativeProjectUri); + destinationRelativePath = utils.trimUri(projectRelativeUri, vscode.Uri.file(destinationRelativePath)); - if (parent) { - if (nodes[i].getAttribute(xmlAttribute) === utils.convertSlashesForSqlProj(includeString)) { - parent.removeChild(nodes[i]); - - // delete ItemGroup if this was the only entry - // only want element nodes, not text nodes - const otherChildren = Array.from(parent.childNodes).filter((c: ChildNode) => c.childNodes); - - if (otherChildren.length === 0) { - parent.parentNode?.removeChild(parent); - } - - return true; - } - } + if (originalRelativePath === destinationRelativePath) { + return { success: true, errorMessage: '' }; } - return false; - } + let result; - /** - * Delete a Remove node from the sqlproj, ex: - * @param xmlTag The XML tag of the node (Build, None, PreDeploy, PostDeploy) - * @param relativePath The relative path of the previously excluded file - */ - private async undoExcludeFileFromProjFile(xmlTag: string, relativePath: string): Promise { - const nodes = this.projFileXmlDoc!.documentElement.getElementsByTagName(xmlTag); - if (this.removeNode(relativePath, nodes, true)) { - await this.serializeToProjFile(this.projFileXmlDoc!); - } - } - - private async addFolderToProjFile(folderPath: string): Promise { - if (this.isSdkStyleProject) { - // if there's a folder entry for the folder containing this folder, remove it from the sqlproj because the folder will now be - // included by the glob that includes this folder (same as how csproj does it) - const folders = await this.foldersListedInSqlproj(); - folders.forEach(folder => { - const trimmedUri = utils.trimChars(utils.trimUri(Uri.file(utils.getPlatformSafeFileEntryPath(folder)), Uri.file(utils.getPlatformSafeFileEntryPath(folderPath))), '/'); - const basename = path.basename(utils.getPlatformSafeFileEntryPath(folderPath)); - if (trimmedUri === basename) { - // remove folder entry from sqlproj - this.removeFolderNode(folder); - } - }); - } - - const newFolderNode = this.projFileXmlDoc!.createElement(constants.Folder); - newFolderNode.setAttribute(constants.Include, utils.convertSlashesForSqlProj(folderPath)); - - this.findOrCreateItemGroup(constants.Folder).appendChild(newFolderNode); - } - - private async removeFolderFromProjFile(folderPath: string): Promise { - let deleted = this.removeFolderNode(folderPath); - - // TODO: consider removing this check when working on migration scenario. If a user converts to an SDK-style project and adding this - // exclude XML doesn't hurt for non-SDK-style projects, then it might be better to just it anyway so that they don't have to exclude the folder - // again when they convert to an SDK-style project - if (this.isSdkStyleProject) { - // update sqlproj if a node was deleted and load files and folders again - await this.writeToSqlProjAndUpdateFilesFolders(); - - // get latest folders to see if it still exists - const currentFolders = await this.readFolders(); - - // add exclude entry if it's still in the current folders - if (currentFolders.find(f => f.relativePath === utils.convertSlashesForSqlProj(folderPath))) { - const removeFileNode = this.projFileXmlDoc!.createElement(constants.Build); - removeFileNode.setAttribute(constants.Remove, utils.convertSlashesForSqlProj(folderPath + '**')); - this.findOrCreateItemGroup(constants.Build).appendChild(removeFileNode); - - // write changes and update files so everything is up to date for the next removal - await this.writeToSqlProjAndUpdateFilesFolders(); - } - - deleted = true; - } - - if (!deleted) { - throw new Error(constants.unableToFindObject(folderPath, constants.folderObject)); - } - } - - private removeFolderNode(folderPath: string): boolean { - const folderNodes = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.Folder); - let deleted = this.removeNode(folderPath, folderNodes); - - // if it wasn't deleted, try deleting the folder path without trailing backslash - // since sometimes SSDT adds folders without a trailing \ - if (!deleted) { - deleted = this.removeNode(utils.trimChars(folderPath, '\\'), folderNodes); - } - - return deleted; - } - - private async writeToSqlProjAndUpdateFilesFolders(): Promise { - await this.serializeToProjFile(this.projFileXmlDoc!); - const projFileText = await fs.readFile(this._projectFilePath); - this.projFileXmlDoc = new xmldom.DOMParser().parseFromString(projFileText.toString()); - this._files = await this.readFilesInProject(); - this.files.push(...(await this.readFolders())); - } - - private removeSqlCmdVariableFromProjFile(variableName: string): void { - const sqlCmdVariableNodes = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.SqlCmdVariable); - const deleted = this.removeNode(variableName, sqlCmdVariableNodes); - - if (!deleted) { - throw new Error(constants.unableToFindSqlCmdVariable(variableName)); - } - } - - private removeDatabaseReferenceFromProjFile(databaseReferenceEntry: IDatabaseReferenceProjectEntry): void { - const elementTag = databaseReferenceEntry instanceof SqlProjectReferenceProjectEntry ? constants.ProjectReference : constants.ArtifactReference; - const artifactReferenceNodes = this.projFileXmlDoc!.documentElement.getElementsByTagName(elementTag); - const deleted = this.removeNode(databaseReferenceEntry.pathForSqlProj(), artifactReferenceNodes); - - // also delete SSDT reference if it's a system db reference - if (databaseReferenceEntry instanceof SystemDatabaseReferenceProjectEntry) { - const ssdtPath = databaseReferenceEntry.ssdtPathForSqlProj(); - this.removeNode(ssdtPath, artifactReferenceNodes); - } - - if (!deleted) { - throw new Error(constants.unableToFindDatabaseReference(databaseReferenceEntry.databaseName)); - } - } - - private async addSystemDatabaseReferenceToProjFile(entry: SystemDatabaseReferenceProjectEntry): Promise { - const systemDbReferenceNode = this.projFileXmlDoc!.createElement(constants.ArtifactReference); - - // if it's a system database reference, we'll add an additional node with the SSDT location of the dacpac later - systemDbReferenceNode.setAttribute(constants.Condition, constants.NetCoreCondition); - systemDbReferenceNode.setAttribute(constants.Include, entry.pathForSqlProj()); - await this.addDatabaseReferenceChildren(systemDbReferenceNode, entry); - this.findOrCreateItemGroup(constants.ArtifactReference).appendChild(systemDbReferenceNode); - - // add a reference to the system dacpac in SSDT if it's a system db - const ssdtReferenceNode = this.projFileXmlDoc!.createElement(constants.ArtifactReference); - ssdtReferenceNode.setAttribute(constants.Condition, constants.NotNetCoreCondition); - ssdtReferenceNode.setAttribute(constants.Include, entry.ssdtPathForSqlProj()); - await this.addDatabaseReferenceChildren(ssdtReferenceNode, entry); - this.findOrCreateItemGroup(constants.ArtifactReference).appendChild(ssdtReferenceNode); - } - - private async addDatabaseReferenceToProjFile(entry: IDatabaseReferenceProjectEntry): Promise { - if (entry instanceof SystemDatabaseReferenceProjectEntry) { - await this.addSystemDatabaseReferenceToProjFile(entry); - } else if (entry instanceof SqlProjectReferenceProjectEntry) { - const referenceNode = this.projFileXmlDoc!.createElement(constants.ProjectReference); - referenceNode.setAttribute(constants.Include, entry.pathForSqlProj()); - this.addProjectReferenceChildren(referenceNode, entry); - await this.addDatabaseReferenceChildren(referenceNode, entry); - this.findOrCreateItemGroup(constants.ProjectReference).appendChild(referenceNode); + if (node instanceof SqlObjectFileNode) { + result = await this.sqlProjService.moveSqlObjectScript(this.projectFilePath, destinationRelativePath, originalRelativePath) + } else if (node instanceof PreDeployNode) { + result = await this.sqlProjService.movePreDeploymentScript(this.projectFilePath, destinationRelativePath, originalRelativePath) + } else if (node instanceof PostDeployNode) { + result = await this.sqlProjService.movePostDeploymentScript(this.projectFilePath, destinationRelativePath, originalRelativePath) + } else if (node instanceof NoneNode || node instanceof PublishProfileNode) { + result = await this.sqlProjService.moveNoneItem(this.projectFilePath, destinationRelativePath, originalRelativePath); } else { - const referenceNode = this.projFileXmlDoc!.createElement(constants.ArtifactReference); - referenceNode.setAttribute(constants.Include, entry.pathForSqlProj()); - await this.addDatabaseReferenceChildren(referenceNode, entry); - this.findOrCreateItemGroup(constants.ArtifactReference).appendChild(referenceNode); + result = { success: false, errorMessage: constants.unhandledMoveNode } } - if (!this.databaseReferenceExists(entry)) { - this._databaseReferences.push(entry); - } - } - - private databaseReferenceExists(entry: IDatabaseReferenceProjectEntry): boolean { - const found = this._databaseReferences.find(reference => reference.pathForSqlProj() === entry.pathForSqlProj()) !== undefined; - return found; - } - - private async addDatabaseReferenceChildren(referenceNode: Element, entry: IDatabaseReferenceProjectEntry): Promise { - const suppressMissingDependenciesErrorNode = this.projFileXmlDoc!.createElement(constants.SuppressMissingDependenciesErrors); - const suppressMissingDependenciesErrorTextNode = this.projFileXmlDoc!.createTextNode(entry.suppressMissingDependenciesErrors ? constants.True : constants.False); - suppressMissingDependenciesErrorNode.appendChild(suppressMissingDependenciesErrorTextNode); - referenceNode.appendChild(suppressMissingDependenciesErrorNode); - - if ((entry).databaseSqlCmdVariable) { - const databaseSqlCmdVariableElement = this.projFileXmlDoc!.createElement(constants.DatabaseSqlCmdVariable); - const databaseSqlCmdVariableTextNode = this.projFileXmlDoc!.createTextNode((entry).databaseSqlCmdVariable!); - databaseSqlCmdVariableElement.appendChild(databaseSqlCmdVariableTextNode); - referenceNode.appendChild(databaseSqlCmdVariableElement); - - // add SQLCMD variable - await this.addSqlCmdVariable((entry).databaseSqlCmdVariable!, (entry).databaseVariableLiteralValue!); - } else if (entry.databaseVariableLiteralValue) { - const databaseVariableLiteralValueElement = this.projFileXmlDoc!.createElement(constants.DatabaseVariableLiteralValue); - const databaseTextNode = this.projFileXmlDoc!.createTextNode(entry.databaseVariableLiteralValue); - databaseVariableLiteralValueElement.appendChild(databaseTextNode); - referenceNode.appendChild(databaseVariableLiteralValueElement); - } - - if ((entry).serverSqlCmdVariable) { - const serverSqlCmdVariableElement = this.projFileXmlDoc!.createElement(constants.ServerSqlCmdVariable); - const serverSqlCmdVariableTextNode = this.projFileXmlDoc!.createTextNode((entry).serverSqlCmdVariable!); - serverSqlCmdVariableElement.appendChild(serverSqlCmdVariableTextNode); - referenceNode.appendChild(serverSqlCmdVariableElement); - - // add SQLCMD variable - await this.addSqlCmdVariable((entry).serverSqlCmdVariable!, (entry).serverName!); - } - } - - private addProjectReferenceChildren(referenceNode: Element, 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 async addSqlCmdVariableToProjFile(entry: SqlCmdVariableProjectEntry): Promise { - // Remove any entries with the same variable name. It'll be replaced with a new one - if (Object.keys(this._sqlCmdVariables).includes(entry.variableName)) { - await this.removeFromProjFile(entry); - } - - const sqlCmdVariableNode = this.projFileXmlDoc!.createElement(constants.SqlCmdVariable); - sqlCmdVariableNode.setAttribute(constants.Include, entry.variableName); - this.addSqlCmdVariableChildren(sqlCmdVariableNode, entry); - this.findOrCreateItemGroup(constants.SqlCmdVariable).appendChild(sqlCmdVariableNode); - - // add to the project's loaded sqlcmd variables - this._sqlCmdVariables[entry.variableName] = entry.defaultValue; - } - - private addSqlCmdVariableChildren(sqlCmdVariableNode: Element, entry: SqlCmdVariableProjectEntry): void { - // add default value - const defaultValueNode = this.projFileXmlDoc!.createElement(constants.DefaultValue); - const defaultValueText = this.projFileXmlDoc!.createTextNode(entry.defaultValue); - defaultValueNode.appendChild(defaultValueText); - sqlCmdVariableNode.appendChild(defaultValueNode); - - // add value node which is in the format $(SqlCmdVar__x) - const valueNode = this.projFileXmlDoc!.createElement(constants.Value); - const valueText = this.projFileXmlDoc!.createTextNode(`$(SqlCmdVar__${this.getNextSqlCmdVariableCounter()})`); - valueNode.appendChild(valueText); - sqlCmdVariableNode.appendChild(valueNode); - } - - /** - * returns the next number that should be used for the new SqlCmd Variable. Old numbers don't get reused even if a SqlCmd Variable - * gets removed from the project - */ - private getNextSqlCmdVariableCounter(): number { - const sqlCmdVariableNodes = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.SqlCmdVariable); - let highestNumber = 0; - - for (let i = 0; i < sqlCmdVariableNodes.length; i++) { - const value: string = sqlCmdVariableNodes[i].getElementsByTagName(constants.Value)[0].childNodes[0].nodeValue!; - const number = parseInt(value.substring(13).slice(0, -1)); // want the number x in $(SqlCmdVar__x) - - // incremement the counter if there's already a variable with the same number or greater - if (number > highestNumber) { - highestNumber = number; - } - } - - return highestNumber + 1; - } - - private async updateImportedTargetsToProjFile(condition: string, projectAttributeVal: string, oldImportNode?: Element): Promise { - const importNode = this.projFileXmlDoc!.createElement(constants.Import); - importNode.setAttribute(constants.Condition, condition); - importNode.setAttribute(constants.Project, projectAttributeVal); - - if (oldImportNode) { - this.projFileXmlDoc!.documentElement.replaceChild(importNode, oldImportNode); - } - else { - this.projFileXmlDoc!.documentElement.appendChild(importNode); - this._importedTargets.push(projectAttributeVal); // Add new import target to the list - } - - await this.serializeToProjFile(this.projFileXmlDoc!); - return importNode; - } - - private async updatePackageReferenceInProjFile(): Promise { - const packageRefNode = this.projFileXmlDoc!.createElement(constants.PackageReference); - packageRefNode.setAttribute(constants.Condition, constants.NetCoreCondition); - packageRefNode.setAttribute(constants.Include, constants.NETFrameworkAssembly); - packageRefNode.setAttribute(constants.Version, constants.VersionNumber); - packageRefNode.setAttribute(constants.PrivateAssets, constants.All); - - this.findOrCreateItemGroup(constants.PackageReference).appendChild(packageRefNode); - - await this.serializeToProjFile(this.projFileXmlDoc!); - } - - public containsSSDTOnlySystemDatabaseReferences(): boolean { - for (let r = 0; r < this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ArtifactReference).length; r++) { - const currentNode = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ArtifactReference)[r]; - if (currentNode.getAttribute(constants.Condition) !== constants.NetCoreCondition && currentNode.getAttribute(constants.Condition) !== constants.NotNetCoreCondition - && currentNode.getAttribute(constants.Include)?.includes(constants.DacpacRootPath)) { - return true; - } - } - - return false; - } - - /** - * Update system db references to have the ADS and SSDT paths to the system dacpacs - */ - public async updateSystemDatabaseReferencesInProjFile(): Promise { - // find all system database references - for (let r = 0; r < this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ArtifactReference).length; r++) { - const currentNode = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ArtifactReference)[r]; - if (!currentNode.getAttribute(constants.Condition) && currentNode.getAttribute(constants.Include)?.includes(constants.DacpacRootPath)) { - // get name of system database - const systemDb = currentNode.getAttribute(constants.Include)?.includes(constants.master) ? SystemDatabase.master : SystemDatabase.msdb; - - // get name - const nameNodes = currentNode.getElementsByTagName(constants.DatabaseVariableLiteralValue); - const databaseVariableName = nameNodes[0].childNodes[0]?.nodeValue!; - - // get suppressMissingDependenciesErrors - const suppressMissingDependenciesErrorNode = currentNode.getElementsByTagName(constants.SuppressMissingDependenciesErrors); - const suppressMissingDependences = suppressMissingDependenciesErrorNode[0].childNodes[0].nodeValue === constants.True; - - // TODO Two issues here : - // 1. If there are multiple ItemGroups with ArtifactReference items then we won't clean up until all items are removed - // 2. If the ItemGroup has other non-ArtifactReference items in it then those will be deleted - // Right now we assume that this ItemGroup is not manually edited so it's safe to ignore these - if (this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ArtifactReference).length === 1) { - // delete entire ItemGroup if there aren't any other children - this.projFileXmlDoc!.documentElement.removeChild(currentNode.parentNode!); - } else { - this.projFileXmlDoc!.documentElement.removeChild(currentNode); - } - - // remove from database references because it'll get added again later - this._databaseReferences.splice(this._databaseReferences.findIndex(n => n.databaseName === (systemDb === SystemDatabase.master ? constants.master : constants.msdb)), 1); - - await this.addSystemDatabaseReference({ databaseName: databaseVariableName, systemDb: systemDb, suppressMissingDependenciesErrors: suppressMissingDependences }); - } - } - - TelemetryReporter.createActionEvent(TelemetryViews.ProjectController, TelemetryActions.updateSystemDatabaseReferencesInProjFile) - .withAdditionalMeasurements({ referencesCount: this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ArtifactReference).length }) - .send(); - } - - private async addToProjFile(entry: ProjectEntry, xmlTag?: string, attributes?: Map): Promise { - switch (entry.type) { - case EntryType.File: - await this.addFileToProjFile((entry).relativePath, xmlTag ? xmlTag : constants.Build, attributes); - break; - case EntryType.Folder: - await this.addFolderToProjFile((entry).relativePath); - break; - case EntryType.DatabaseReference: - await this.addDatabaseReferenceToProjFile(entry); - break; - case EntryType.SqlCmdVariable: - await this.addSqlCmdVariableToProjFile(entry); - break; // not required but adding so that we dont miss when we add new items - } - - await this.serializeToProjFile(this.projFileXmlDoc!); - } - - private async removeFromProjFile(entries: IProjectEntry | IProjectEntry[]): Promise { - if (!Array.isArray(entries)) { - entries = [entries]; - } - - // remove any folders first, otherwise unnecessary Build remove entries might get added for sdk style - // projects to exclude both the folder and the files in the folder - const folderEntries = entries.filter(e => e.type === EntryType.Folder); - for (const folder of folderEntries) { - await this.removeFolderFromProjFile((folder).relativePath); - } - - entries = entries.filter(e => e.type !== EntryType.Folder); - - for (const entry of entries) { - switch (entry.type) { - case EntryType.File: - await this.removeFileFromProjFile((entry).relativePath); - break; - case EntryType.DatabaseReference: - this.removeDatabaseReferenceFromProjFile(entry); - break; - case EntryType.SqlCmdVariable: - this.removeSqlCmdVariableFromProjFile((entry).variableName); - break; // not required but adding so that we dont miss when we add new items - } - } - - await this.serializeToProjFile(this.projFileXmlDoc!); - } - - private async serializeToProjFile(projFileContents: Document): Promise { - let xml = new xmldom.XMLSerializer().serializeToString(projFileContents); - xml = xmlFormat(xml, { - collapseContent: true, - indentation: ' ', - lineSeparator: os.EOL, - whiteSpaceAtEndOfSelfclosingTag: true - }); - - await fs.writeFile(this._projectFilePath, xml); - - // update projFileXmlDoc since the file was updated - this.projFileXmlDoc = new xmldom.DOMParser().parseFromString(xml); - } - - /** - * Adds the list of sql files and directories to the project, and saves the project file - * - * @param list list of files and folder Uris. Files and folders must already exist. No files or folders will be added if any do not exist. - */ - public async addToProject(list: Uri[]): Promise { - // verify all files/folders exist. If not all exist, none will be added - for (let file of list) { - const exists = await utils.exists(file.fsPath); - - if (!exists) { - throw new Error(constants.fileOrFolderDoesNotExist(file.fsPath)); - } - } - - for (let file of list) { - const relativePath = utils.trimChars(utils.trimUri(Uri.file(this._projectFilePath), file), '/'); - - if (relativePath.length > 0) { - const fileStat = await fs.stat(file.fsPath); - - if (fileStat.isFile() && file.fsPath.toLowerCase().endsWith(constants.sqlFileExtension)) { - await this.addScriptItem(relativePath); - } else if (fileStat.isDirectory()) { - await this.addFolderItem(relativePath); - } - } - } - } - - /** - * Adds a value to the project property, where multiple values are separated by semicolon. - * If property does not exist, the new one will be added. Otherwise a value will be appended - * to the existing property. - * - * @param propertyName Name of the project property - * @param valueToAdd Value to add to the project property. Values containing semicolon are not supported - * @param caseSensitive Flag that indicates whether to use case-sensitive comparison when determining, if value is already present - */ - private async addValueToCollectionProjectProperty(propertyName: string, valueToAdd: string, caseSensitive: boolean = false): Promise { - if (valueToAdd.includes(';')) { - throw new Error(constants.invalidProjectPropertyValueProvided(valueToAdd)); - } - - let collectionValues = this.getCollectionProjectPropertyValue(propertyName); - - // Respect case-sensitivity flag - const normalizedValueToAdd = caseSensitive ? valueToAdd : valueToAdd.toUpperCase(); - - // Only add value if it is not present yet - if (collectionValues.findIndex(value => (caseSensitive ? value : value.toUpperCase()) === normalizedValueToAdd) < 0) { - collectionValues.push(valueToAdd); - await this.setProjectPropertyValue(propertyName, collectionValues.join(';')); - } - } - - /** - * Removes a value from the project property, where multiple values are separated by semicolon. - * If property becomes empty after the removal of the value, then it will be completely removed - * from the project file. - * If value appears in the collection multiple times, only the first occurance will be removed. - * - * @param propertyName Name of the project property - * @param valueToRemove Value to remove from the project property. Values containing semicolon are not supported - * @param caseSensitive Flag that indicates whether to use case-sensitive comparison when removing the value - */ - protected async removeValueFromCollectionProjectProperty(propertyName: string, valueToRemove: string, caseSensitive: boolean = false): Promise { - if (this.projFileXmlDoc === undefined) { - return; - } - - if (valueToRemove.includes(';')) { - throw new Error(constants.invalidProjectPropertyValueProvided(valueToRemove)); - } - - let collectionValues = this.getCollectionProjectPropertyValue(propertyName); - - // Respect case-sensitivity flag - const normalizedValueToRemove = caseSensitive ? valueToRemove : valueToRemove.toUpperCase(); - - const indexToRemove = - collectionValues.findIndex(value => (caseSensitive ? value : value.toUpperCase()) === normalizedValueToRemove); - - if (indexToRemove >= 0) { - collectionValues.splice(indexToRemove, 1); - - if (collectionValues.length === 0) { - // No elements left in the collection - remove the property entirely - this.removeProjectPropertyTag(propertyName); - await this.serializeToProjFile(this.projFileXmlDoc); - } else { - // Update property value with modified collection - await this.setProjectPropertyValue(propertyName, collectionValues.join(';')); - } - } - } - - /** - * Evaluates the value of the property item in the loaded project. - * - * @param propertyName Name of the property item to evaluate. - * @returns Value of the property or `undefined`, if property is missing. - */ - private evaluateProjectPropertyValue(propertyName: string): string | undefined; - - /** - * Evaluates the value of the property item in the loaded project. - * - * @param propertyName Name of the property item to evaluate. - * @param defaultValue Default value to return, if property is not set. - * @returns Value of the property or `defaultValue`, if property is missing. - */ - private evaluateProjectPropertyValue(propertyName: string, defaultValue: string): string; - - /** - * Evaluates the value of the property item in the loaded project. - * - * @param propertyName Name of the property item to evaluate. - * @param defaultValue Default value to return, if property is not set. - * @returns Value of the property or `defaultValue`, if property is missing. - */ - private evaluateProjectPropertyValue(propertyName: string, defaultValue?: string): string | undefined { - // TODO: Currently we simply read the value of the first matching element. The code should be updated to: - // 1) Narrow it down to items under only - // 2) Respect the `Condition` attribute on group and property itself - // 3) Evaluate any expressions within the property value - - // Check if property is set in the project - const propertyElements = this.projFileXmlDoc!.getElementsByTagName(propertyName); - if (propertyElements.length === 0) { - return defaultValue; - } - - // Try to extract the value from the first matching element - const firstPropertyElement = propertyElements[0]; - if (firstPropertyElement.childNodes.length !== 1) { - // Property items are expected to have simple string content - throw new Error(constants.invalidProjectPropertyValueInSqlProj(propertyName)); - } - - return firstPropertyElement.childNodes[0].nodeValue!; - } - - /** - * Retrieves all semicolon-separated values specified in the project property. - * - * @param propertyName Name of the project property - * @returns Array of semicolon-separated values specified in the property - */ - private getCollectionProjectPropertyValue(propertyName: string): string[] { - const propertyValue = this.evaluateProjectPropertyValue(propertyName); - if (propertyValue === undefined) { - return []; - } - - return propertyValue.split(';') - .filter(value => value.length > 0); - } - - /** - * Sets the value of the project property. - * - * @param propertyName Name of the project property - * @param propertyValue New value of the project property - */ - private async setProjectPropertyValue(propertyName: string, propertyValue: string): Promise { - if (this.projFileXmlDoc === undefined) { - return; - } - - let propertyElement: Element | undefined; - - // Try to find an existing property element with the requested name. - // There could be multiple elements in different property groups or even within the - // same property group (different `Condition` attribute, for example). As of now, - // we always choose the first one and update it. - const propertyGroups = this.projFileXmlDoc.getElementsByTagName(constants.PropertyGroup); - for (let propertyGroupIndex = 0; propertyGroupIndex < propertyGroups.length; ++propertyGroupIndex) { - const propertyElements = propertyGroups[propertyGroupIndex].getElementsByTagName(propertyName); - - if (propertyElements.length > 0) { - propertyElement = propertyElements[0]; - break; - } - } - - if (propertyElement === undefined) { - // If existing property element was not found, then we add a new one - propertyElement = this.addProjectPropertyTag(propertyName); - } - - // Ensure property element was found or successfully added - if (propertyElement) { - if (propertyElement.childNodes.length > 0) { - propertyElement.replaceChild(this.projFileXmlDoc.createTextNode(propertyValue), propertyElement.childNodes[0]); - } else { - propertyElement.appendChild(this.projFileXmlDoc.createTextNode(propertyValue)); - } - - await this.serializeToProjFile(this.projFileXmlDoc); - } - } - - /** - * Adds an empty project property tag. - * - * @param propertyTag Tag to add - * @returns Added HTMLElement tag - */ - private addProjectPropertyTag(propertyTag: string): HTMLElement | undefined { - if (this.projFileXmlDoc === undefined) { - return; - } - - const propertyGroups = this.projFileXmlDoc.getElementsByTagName(constants.PropertyGroup); - let propertyGroup = propertyGroups.length > 0 ? propertyGroups[0] : undefined; - if (propertyGroup === undefined) { - propertyGroup = this.projFileXmlDoc.createElement(constants.PropertyGroup); - this.projFileXmlDoc.documentElement?.appendChild(propertyGroup); - } - - const propertyElement = this.projFileXmlDoc.createElement(propertyTag); - propertyGroup.appendChild(propertyElement); - return propertyElement; - } - - /** - * Removes first occurrence of the project property. - * - * @param propertyTag Tag to remove - */ - private removeProjectPropertyTag(propertyTag: string) { - if (this.projFileXmlDoc === undefined) { - return; - } - - const propertyGroups = this.projFileXmlDoc.getElementsByTagName(constants.PropertyGroup); - - for (let propertyGroupIndex in propertyGroups) { - let propertiesWithTagName = propertyGroups[propertyGroupIndex].getElementsByTagName(propertyTag); - if (propertiesWithTagName.length > 0) { - propertiesWithTagName[0].parentNode?.removeChild(propertiesWithTagName[0]); - return; - } - } - } - - /** - * Adds all folders in the path to the project and saves the project file, if provided path is under the project folder. - * If path is outside the project folder, then no action is taken. - * - * @param relativeFolderPath Relative folder path to add folders from. - * @returns Project entry for the last folder in the path, if path is under the project folder; otherwise `undefined`. - */ - private async ensureFolderItems(relativeFolderPath: string): Promise { - if (!relativeFolderPath) { - return; - } - - const absoluteFolderPath = path.join(this.projectFolderPath, relativeFolderPath); - const normalizedProjectFolderPath = path.normalize(this.projectFolderPath); - - // Only add folders within the project folder. When adding files outside the project folder, - // they should be copied to the project root and there will be no additional folders to add. - if (!absoluteFolderPath.toUpperCase().startsWith(normalizedProjectFolderPath.toUpperCase())) { - return; - } - - // If folder doesn't exist, create it - await fs.mkdir(absoluteFolderPath, { recursive: true }); - - // for SDK style projects, only add this folder to the sqlproj if needed - // intermediate folders don't need to be added in the sqlproj - if (this.isSdkStyleProject) { - let folderEntry = this.files.find(f => utils.ensureTrailingSlash(f.relativePath.toUpperCase()) === utils.ensureTrailingSlash((relativeFolderPath.toUpperCase()))); - - if (!folderEntry) { - folderEntry = this.createFileProjectEntry(utils.ensureTrailingSlash(relativeFolderPath), EntryType.Folder); - this.files.push(folderEntry); - await this.addToProjFile(folderEntry); - } - - return folderEntry; - } - - // Add project file entries for all folders in the path. - // SSDT expects all folders to be explicitly listed in the project file, so we construct - // folder paths for all intermediate folders and ensure they are present in the project as well. - // We do not use `path.relative` here, because it may return '.' if paths are the same, - // but in our case we actually want an empty string, that will result in an empty segments - // array and nothing will be added. - const relativePath = utils.convertSlashesForSqlProj(absoluteFolderPath.substring(normalizedProjectFolderPath.length)); - const pathSegments = utils.trimChars(relativePath, ' \\').split(constants.SqlProjPathSeparator); - let folderEntryPath = ''; - let folderEntry: FileProjectEntry | undefined; - - // Add folder items for all segments, including the requested folder itself - for (let segment of pathSegments) { - if (segment) { - folderEntryPath += segment + constants.SqlProjPathSeparator; - folderEntry = - this.files.find(f => utils.ensureTrailingSlash(f.relativePath.toUpperCase()) === folderEntryPath.toUpperCase()); - - if (!folderEntry) { - // If there is no item for the folder - add it - folderEntry = this.createFileProjectEntry(folderEntryPath, EntryType.Folder); - this.files.push(folderEntry); - await this.addToProjFile(folderEntry); - } - } - } - - return folderEntry; + return result; } } - -export const reservedProjectFolders = ['Properties', 'Data Sources', 'Database References']; diff --git a/extensions/sql-database-projects/src/models/projectEntry.ts b/extensions/sql-database-projects/src/models/projectEntry.ts index 257606d32f..9c13e0d44a 100644 --- a/extensions/sql-database-projects/src/models/projectEntry.ts +++ b/extensions/sql-database-projects/src/models/projectEntry.ts @@ -44,25 +44,29 @@ export class FileProjectEntry extends ProjectEntry implements IFileProjectEntry } export class DacpacReferenceProjectEntry extends FileProjectEntry implements IDatabaseReferenceProjectEntry { + databaseSqlCmdVariableValue?: string; + databaseSqlCmdVariableName?: string; databaseVariableLiteralValue?: string; - databaseSqlCmdVariable?: string; - serverName?: string; - serverSqlCmdVariable?: string; + serverSqlCmdVariableName?: string; + serverSqlCmdVariableValue?: string; suppressMissingDependenciesErrors: boolean; constructor(settings: IDacpacReferenceSettings) { - super(settings.dacpacFileLocation, '', EntryType.DatabaseReference); - this.databaseSqlCmdVariable = settings.databaseVariable; - this.databaseVariableLiteralValue = settings.databaseName; - this.serverName = settings.serverName; - this.serverSqlCmdVariable = settings.serverVariable; + super(settings.dacpacFileLocation, /* relativePath doesn't get set for database references */ '', EntryType.DatabaseReference); this.suppressMissingDependenciesErrors = settings.suppressMissingDependenciesErrors; + + this.databaseVariableLiteralValue = settings.databaseVariableLiteralValue; + this.databaseSqlCmdVariableName = settings.databaseName; + this.databaseSqlCmdVariableValue = settings.databaseVariable; + + this.serverSqlCmdVariableName = settings.serverName; + this.serverSqlCmdVariableValue = settings.serverVariable; } /** * File name that gets displayed in the project tree */ - public get databaseName(): string { + public get referenceName(): string { return path.parse(utils.getPlatformSafeFileEntryPath(this.fsUri.fsPath)).name; } @@ -73,49 +77,44 @@ export class DacpacReferenceProjectEntry extends FileProjectEntry implements IDa } export class SystemDatabaseReferenceProjectEntry extends FileProjectEntry implements IDatabaseReferenceProjectEntry { - constructor(uri: Uri, public ssdtUri: Uri, public databaseVariableLiteralValue: string | undefined, public suppressMissingDependenciesErrors: boolean) { - super(uri, '', EntryType.DatabaseReference); + constructor(public referenceName: string, public databaseVariableLiteralValue: string | undefined, public suppressMissingDependenciesErrors: boolean) { + super(Uri.file(referenceName), referenceName, EntryType.DatabaseReference); } /** - * File name that gets displayed in the project tree + * Returns the name of the system database - this is used for deleting the system database reference */ - public get databaseName(): string { - return path.parse(utils.getPlatformSafeFileEntryPath(this.fsUri.fsPath)).name; - } - public override pathForSqlProj(): string { - // need to remove the leading slash for system database path for build to work on Windows - return utils.convertSlashesForSqlProj(this.fsUri.path.substring(1)); - } - - public ssdtPathForSqlProj(): string { - // need to remove the leading slash for system database path for build to work on Windows - return utils.convertSlashesForSqlProj(this.ssdtUri.path.substring(1)); + return this.referenceName; } } export class SqlProjectReferenceProjectEntry extends FileProjectEntry implements IDatabaseReferenceProjectEntry { - projectName: string; - projectGuid: string; - databaseVariableLiteralValue?: string; - databaseSqlCmdVariable?: string; - serverName?: string; - serverSqlCmdVariable?: string; - suppressMissingDependenciesErrors: boolean; + public projectName: string; + public projectGuid: string; + public databaseVariableLiteralValue?: string; + public databaseSqlCmdVariableName?: string; + public databaseSqlCmdVariableValue?: string; + public serverSqlCmdVariableName?: string; + public serverSqlCmdVariableValue?: string; + public suppressMissingDependenciesErrors: boolean; constructor(settings: IProjectReferenceSettings) { - super(settings.projectRelativePath!, '', EntryType.DatabaseReference); + super(settings.projectRelativePath!, /* relativePath doesn't get set for database references */ '', 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; + + this.databaseVariableLiteralValue = settings.databaseVariableLiteralValue; + this.databaseSqlCmdVariableName = settings.databaseName; + this.databaseSqlCmdVariableValue = settings.databaseVariable; + + this.serverSqlCmdVariableName = settings.serverName; + this.serverSqlCmdVariableValue = settings.serverVariable; } - public get databaseName(): string { + public get referenceName(): string { return this.projectName; } @@ -136,8 +135,3 @@ export enum DatabaseReferenceLocation { differentDatabaseSameServer, differentDatabaseDifferentServer } - -export enum SystemDatabase { - master, - msdb -} diff --git a/extensions/sql-database-projects/src/models/tree/baseTreeItem.ts b/extensions/sql-database-projects/src/models/tree/baseTreeItem.ts index 37e344f791..c24d6ad0e4 100644 --- a/extensions/sql-database-projects/src/models/tree/baseTreeItem.ts +++ b/extensions/sql-database-projects/src/models/tree/baseTreeItem.ts @@ -5,6 +5,7 @@ import * as vscode from 'vscode'; import * as path from 'path'; +import { DatabaseProjectItemType } from '../../common/constants'; /** * Base class for an item that appears in the ADS project tree @@ -21,6 +22,10 @@ export abstract class BaseProjectTreeItem { abstract get treeItem(): vscode.TreeItem; + abstract get type(): DatabaseProjectItemType; + + public entryKey?: string; + public get friendlyName(): string { return path.parse(this.relativeProjectUri.path).base; } diff --git a/extensions/sql-database-projects/src/models/tree/databaseReferencesTreeItem.ts b/extensions/sql-database-projects/src/models/tree/databaseReferencesTreeItem.ts index 890c31abc4..9cccd70f10 100644 --- a/extensions/sql-database-projects/src/models/tree/databaseReferencesTreeItem.ts +++ b/extensions/sql-database-projects/src/models/tree/databaseReferencesTreeItem.ts @@ -26,7 +26,6 @@ export class DatabaseReferencesTreeItem extends BaseProjectTreeItem { */ constructor(projectNodeName: string, sqlprojUri: vscode.Uri, databaseReferences: IDatabaseReferenceProjectEntry[]) { super(vscode.Uri.file(path.join(projectNodeName, constants.databaseReferencesNodeName)), sqlprojUri); - this.construct(databaseReferences); } @@ -44,9 +43,13 @@ export class DatabaseReferencesTreeItem extends BaseProjectTreeItem { return this.references; } + public get type(): constants.DatabaseProjectItemType { + return constants.DatabaseProjectItemType.referencesRoot; + } + public get treeItem(): vscode.TreeItem { const refFolderItem = new vscode.TreeItem(this.relativeProjectUri, vscode.TreeItemCollapsibleState.Collapsed); - refFolderItem.contextValue = constants.DatabaseProjectItemType.referencesRoot; + refFolderItem.contextValue = this.type; refFolderItem.iconPath = IconPathHelper.referenceGroup; return refFolderItem; @@ -55,17 +58,22 @@ export class DatabaseReferencesTreeItem extends BaseProjectTreeItem { export class DatabaseReferenceTreeItem extends BaseProjectTreeItem { constructor(private reference: IDatabaseReferenceProjectEntry, referencesNodeRelativeProjectUri: vscode.Uri, sqlprojUri: vscode.Uri) { - super(vscode.Uri.file(path.join(referencesNodeRelativeProjectUri.fsPath, reference.databaseName)), sqlprojUri); + super(vscode.Uri.file(path.join(referencesNodeRelativeProjectUri.fsPath, reference.referenceName)), sqlprojUri); + this.entryKey = this.friendlyName; } public get children(): BaseProjectTreeItem[] { return []; } + public get type(): constants.DatabaseProjectItemType { + return constants.DatabaseProjectItemType.reference; + } + public get treeItem(): vscode.TreeItem { const refItem = new vscode.TreeItem(this.relativeProjectUri, vscode.TreeItemCollapsibleState.None); - refItem.label = this.reference.databaseName; - refItem.contextValue = constants.DatabaseProjectItemType.reference; + refItem.label = this.reference.referenceName; + refItem.contextValue = this.type; refItem.iconPath = IconPathHelper.referenceDatabase; return refItem; diff --git a/extensions/sql-database-projects/src/models/tree/fileFolderTreeItem.ts b/extensions/sql-database-projects/src/models/tree/fileFolderTreeItem.ts index c7b390a0e7..6c84e0da1b 100644 --- a/extensions/sql-database-projects/src/models/tree/fileFolderTreeItem.ts +++ b/extensions/sql-database-projects/src/models/tree/fileFolderTreeItem.ts @@ -16,19 +16,25 @@ import { IconPathHelper } from '../../common/iconHelper'; export class FolderNode extends BaseProjectTreeItem { public fileChildren: { [childName: string]: (FolderNode | FileNode) } = {}; public fileSystemUri: vscode.Uri; + public override entryKey: string; - constructor(folderPath: vscode.Uri, sqlprojUri: vscode.Uri) { + constructor(folderPath: vscode.Uri, sqlprojUri: vscode.Uri, entryKey: string) { super(fsPathToProjectUri(folderPath, sqlprojUri), sqlprojUri); this.fileSystemUri = folderPath; + this.entryKey = entryKey; } public get children(): BaseProjectTreeItem[] { return Object.values(this.fileChildren).sort(sortFileFolderNodes); } + public get type(): DatabaseProjectItemType { + return DatabaseProjectItemType.folder; + } + public get treeItem(): vscode.TreeItem { const folderItem = new vscode.TreeItem(this.fileSystemUri, vscode.TreeItemCollapsibleState.Collapsed); - folderItem.contextValue = DatabaseProjectItemType.folder; + folderItem.contextValue = this.type; folderItem.iconPath = IconPathHelper.folder; return folderItem; @@ -40,10 +46,12 @@ export class FolderNode extends BaseProjectTreeItem { */ export abstract class FileNode extends BaseProjectTreeItem { public fileSystemUri: vscode.Uri; + public override entryKey: string; - constructor(filePath: vscode.Uri, sqlprojUri: vscode.Uri) { + constructor(filePath: vscode.Uri, sqlprojUri: vscode.Uri, entryKey: string) { super(fsPathToProjectUri(filePath, sqlprojUri, true), sqlprojUri); this.fileSystemUri = filePath; + this.entryKey = entryKey; } public get children(): BaseProjectTreeItem[] { @@ -68,64 +76,92 @@ export abstract class FileNode extends BaseProjectTreeItem { export class SqlObjectFileNode extends FileNode { public override get treeItem(): vscode.TreeItem { const treeItem = super.treeItem; - treeItem.contextValue = DatabaseProjectItemType.sqlObjectScript; + treeItem.contextValue = this.type; return treeItem; } + + public get type(): DatabaseProjectItemType { + return DatabaseProjectItemType.sqlObjectScript; + } } export class ExternalStreamingJobFileNode extends SqlObjectFileNode { public override get treeItem(): vscode.TreeItem { const treeItem = super.treeItem; - treeItem.contextValue = DatabaseProjectItemType.externalStreamingJob; + treeItem.contextValue = this.type; return treeItem; } + + public override get type(): DatabaseProjectItemType { + return DatabaseProjectItemType.externalStreamingJob; + } } export class TableFileNode extends SqlObjectFileNode { public override get treeItem(): vscode.TreeItem { const treeItem = super.treeItem; - treeItem.contextValue = DatabaseProjectItemType.table; + treeItem.contextValue = this.type; return treeItem; } + + public override get type(): DatabaseProjectItemType { + return DatabaseProjectItemType.table; + } } export class PreDeployNode extends FileNode { public override get treeItem(): vscode.TreeItem { const treeItem = super.treeItem; - treeItem.contextValue = DatabaseProjectItemType.preDeploymentScript; + treeItem.contextValue = this.type; return treeItem; } + + public get type(): DatabaseProjectItemType { + return DatabaseProjectItemType.preDeploymentScript; + } } export class PostDeployNode extends FileNode { public override get treeItem(): vscode.TreeItem { const treeItem = super.treeItem; - treeItem.contextValue = DatabaseProjectItemType.postDeploymentScript; + treeItem.contextValue = this.type; return treeItem; } + + public get type(): DatabaseProjectItemType { + return DatabaseProjectItemType.postDeploymentScript; + } } export class NoneNode extends FileNode { public override get treeItem(): vscode.TreeItem { const treeItem = super.treeItem; - treeItem.contextValue = DatabaseProjectItemType.noneFile; + treeItem.contextValue = this.type; return treeItem; } + + public get type(): DatabaseProjectItemType { + return DatabaseProjectItemType.noneFile; + } } export class PublishProfileNode extends FileNode { public override get treeItem(): vscode.TreeItem { const treeItem = super.treeItem; - treeItem.contextValue = DatabaseProjectItemType.publishProfile; + treeItem.contextValue = this.type; return treeItem; } + + public get type(): DatabaseProjectItemType { + return DatabaseProjectItemType.publishProfile; + } } /** diff --git a/extensions/sql-database-projects/src/models/tree/projectTreeItem.ts b/extensions/sql-database-projects/src/models/tree/projectTreeItem.ts index 7cc31ee092..999e8dd252 100644 --- a/extensions/sql-database-projects/src/models/tree/projectTreeItem.ts +++ b/extensions/sql-database-projects/src/models/tree/projectTreeItem.ts @@ -10,12 +10,13 @@ import * as fileTree from './fileFolderTreeItem'; import { Project } from '../project'; import * as utils from '../../common/utils'; import { DatabaseReferencesTreeItem } from './databaseReferencesTreeItem'; -import { DatabaseProjectItemType, RelativeOuterPath, ExternalStreamingJob, sqlprojExtension, CollapseProjectNodesKey } from '../../common/constants'; +import { DatabaseProjectItemType, RelativeOuterPath, ExternalStreamingJob, sqlprojExtension, CollapseProjectNodesKey, errorPrefix } from '../../common/constants'; import { IconPathHelper } from '../../common/iconHelper'; import { FileProjectEntry } from '../projectEntry'; import { EntryType } from 'sqldbproj'; import { DBProjectConfigurationKey } from '../../tools/netcoreTool'; import { SqlCmdVariablesTreeItem } from './sqlcmdVariableTreeItem'; +import { ProjectType } from 'mssql'; /** * TreeNode root that represents an entire project @@ -51,62 +52,62 @@ export class ProjectRootTreeItem extends BaseProjectTreeItem { public get treeItem(): vscode.TreeItem { const collapsibleState = vscode.workspace.getConfiguration(DBProjectConfigurationKey)[CollapseProjectNodesKey] ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.Expanded; const projectItem = new vscode.TreeItem(this.fileSystemUri, collapsibleState); - projectItem.contextValue = this.project.isSdkStyleProject ? DatabaseProjectItemType.project : DatabaseProjectItemType.legacyProject; + projectItem.contextValue = this.type; projectItem.iconPath = IconPathHelper.databaseProject; projectItem.label = this.projectNodeName; return projectItem; } + public get type(): DatabaseProjectItemType { + return this.project.sqlProjStyle === ProjectType.SdkStyle ? DatabaseProjectItemType.project : DatabaseProjectItemType.legacyProject; + } + /** * Processes the list of files in a project file to constructs the tree */ private construct() { + // folders + // Note: folders must be sorted to ensure that parent folders come before their children + for (const folder of this.project.folders.sort((a, b) => a.relativePath < b.relativePath ? -1 : (a.relativePath > b.relativePath ? 1 : 0))) { + const newNode = new fileTree.FolderNode(folder.fsUri, this.projectFileUri, folder.relativePath); + this.addNode(newNode, folder); + } + // pre deploy scripts for (const preDeployEntry of this.project.preDeployScripts) { - const newNode = new fileTree.PreDeployNode(preDeployEntry.fsUri, this.projectFileUri); + const newNode = new fileTree.PreDeployNode(preDeployEntry.fsUri, this.projectFileUri, preDeployEntry.relativePath); this.addNode(newNode, preDeployEntry); } // post deploy scripts for (const postDeployEntry of this.project.postDeployScripts) { - const newNode = new fileTree.PostDeployNode(postDeployEntry.fsUri, this.projectFileUri); + const newNode = new fileTree.PostDeployNode(postDeployEntry.fsUri, this.projectFileUri, postDeployEntry.relativePath); this.addNode(newNode, postDeployEntry); } // none scripts for (const noneEntry of this.project.noneDeployScripts) { - const newNode = new fileTree.NoneNode(noneEntry.fsUri, this.projectFileUri); + const newNode = new fileTree.NoneNode(noneEntry.fsUri, this.projectFileUri, noneEntry.relativePath); this.addNode(newNode, noneEntry); } // publish profiles for (const publishProfile of this.project.publishProfiles) { - const newNode = new fileTree.PublishProfileNode(publishProfile.fsUri, this.projectFileUri); + const newNode = new fileTree.PublishProfileNode(publishProfile.fsUri, this.projectFileUri, publishProfile.relativePath); this.addNode(newNode, publishProfile); } - // sql object scripts and folders + // sql object scripts for (const entry of this.project.files) { - let newNode: fileTree.FolderNode | fileTree.FileNode; + let newNode: fileTree.FileNode; - switch (entry.type) { - case EntryType.File: - if (entry.sqlObjectType === ExternalStreamingJob) { - newNode = new fileTree.ExternalStreamingJobFileNode(entry.fsUri, this.projectFileUri); - } else if (entry.containsCreateTableStatement) { - newNode = new fileTree.TableFileNode(entry.fsUri, this.projectFileUri); - } - else { - newNode = new fileTree.SqlObjectFileNode(entry.fsUri, this.projectFileUri); - } - - break; - case EntryType.Folder: - newNode = new fileTree.FolderNode(entry.fsUri, this.projectFileUri); - break; - default: - throw new Error(`Unknown EntryType: '${entry.type}'`); + if (entry.sqlObjectType === ExternalStreamingJob) { + newNode = new fileTree.ExternalStreamingJobFileNode(entry.fsUri, this.projectFileUri, entry.relativePath); + } else if (entry.containsCreateTableStatement) { + newNode = new fileTree.TableFileNode(entry.fsUri, this.projectFileUri, entry.relativePath); + } else { + newNode = new fileTree.SqlObjectFileNode(entry.fsUri, this.projectFileUri, entry.relativePath); } this.addNode(newNode, entry); @@ -138,23 +139,26 @@ export class ProjectRootTreeItem extends BaseProjectTreeItem { return this; // if nothing left after trimming the entry itself, must been root } - if (relativePathParts[0] === RelativeOuterPath) { + if (relativePathParts[0] === RelativeOuterPath) { // scripts external to the project folder are always parented by the project root node because external folders aren't supported return this; } - let current: fileTree.FolderNode | ProjectRootTreeItem = this; + let current: fileTree.FolderNode | ProjectRootTreeItem = this; // start with the Project root node - for (const part of relativePathParts) { + for (const part of relativePathParts) { // iterate from the project root, down the path to the entry in question if (current.fileChildren[part] === undefined) { - const parentPath = current instanceof ProjectRootTreeItem ? path.dirname(current.fileSystemUri.fsPath) : current.fileSystemUri.fsPath; - current.fileChildren[part] = new fileTree.FolderNode(vscode.Uri.file(path.join(parentPath, part)), this.projectFileUri); + // DacFx.Projects populates the list of folders with those implicitly included via parentage. + // e.g. and both result in the "MySchema" folder being automatically added, + // even if there's no entry. + // Project tree unit tests need to explicitly include parent folders because they bypass DacFx's logic, or they'll hit this error. + throw new Error(errorPrefix(`All parent nodes for ${relativePathParts} should have already been added.`)); } if (current.fileChildren[part] instanceof fileTree.FileNode) { - return current; + return current; // if we've made it to the node in question, we're done } else { - current = current.fileChildren[part] as fileTree.FolderNode | ProjectRootTreeItem; + current = current.fileChildren[part] as fileTree.FolderNode; // otherwise, shift the current node down, and repeat } } diff --git a/extensions/sql-database-projects/src/models/tree/sqlcmdVariableTreeItem.ts b/extensions/sql-database-projects/src/models/tree/sqlcmdVariableTreeItem.ts index 6b9eb4a7d3..cf06d9810d 100644 --- a/extensions/sql-database-projects/src/models/tree/sqlcmdVariableTreeItem.ts +++ b/extensions/sql-database-projects/src/models/tree/sqlcmdVariableTreeItem.ts @@ -45,9 +45,13 @@ export class SqlCmdVariablesTreeItem extends BaseProjectTreeItem { return this.sqlcmdVariableTreeItems; } + public get type(): constants.DatabaseProjectItemType { + return constants.DatabaseProjectItemType.sqlcmdVariablesRoot; + } + public get treeItem(): vscode.TreeItem { const sqlCmdVariableFolderItem = new vscode.TreeItem(this.relativeProjectUri, vscode.TreeItemCollapsibleState.Collapsed); - sqlCmdVariableFolderItem.contextValue = constants.DatabaseProjectItemType.sqlcmdVariablesRoot; + sqlCmdVariableFolderItem.contextValue = this.type; sqlCmdVariableFolderItem.iconPath = IconPathHelper.sqlCmdVariablesGroup; return sqlCmdVariableFolderItem; @@ -60,16 +64,21 @@ export class SqlCmdVariablesTreeItem extends BaseProjectTreeItem { export class SqlCmdVariableTreeItem extends BaseProjectTreeItem { constructor(private sqlcmdVar: string, sqlCmdNodeRelativeProjectUri: vscode.Uri, sqlprojUri: vscode.Uri,) { super(vscode.Uri.file(path.join(sqlCmdNodeRelativeProjectUri.fsPath, sqlcmdVar)), sqlprojUri); + this.entryKey = this.friendlyName; } public get children(): BaseProjectTreeItem[] { return []; } + public get type(): constants.DatabaseProjectItemType { + return constants.DatabaseProjectItemType.sqlcmdVariable; + } + public get treeItem(): vscode.TreeItem { const sqlcmdVariableItem = new vscode.TreeItem(this.relativeProjectUri, vscode.TreeItemCollapsibleState.None); sqlcmdVariableItem.label = this.sqlcmdVar; - sqlcmdVariableItem.contextValue = constants.DatabaseProjectItemType.sqlcmdVariable; + sqlcmdVariableItem.contextValue = this.type; sqlcmdVariableItem.iconPath = IconPathHelper.sqlCmdVariable; return sqlcmdVariableItem; diff --git a/extensions/sql-database-projects/src/projectProvider/projectProvider.ts b/extensions/sql-database-projects/src/projectProvider/projectProvider.ts index a8cb43ee47..9dddea040e 100644 --- a/extensions/sql-database-projects/src/projectProvider/projectProvider.ts +++ b/extensions/sql-database-projects/src/projectProvider/projectProvider.ts @@ -40,7 +40,7 @@ export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvide */ public async getProjectTreeDataProvider(projectFilePath: vscode.Uri): Promise> { const provider = new SqlDatabaseProjectTreeViewProvider(); - const project = await Project.openProject(projectFilePath.fsPath); + const project = await Project.openProject(projectFilePath.fsPath, true, true); // open project in STS const sqlProjectsService = await getSqlProjectsService(); @@ -112,7 +112,7 @@ export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvide * Opens and loads a .sqlproj file */ public openProject(projectFilePath: string): Promise { - return Project.openProject(projectFilePath); + return Project.openProject(projectFilePath, true, true); } public addItemPrompt(project: sqldbproj.ISqlProject, relativeFilePath: string, options?: sqldbproj.AddItemOptions): Promise { diff --git a/extensions/sql-database-projects/src/sqldbproj.d.ts b/extensions/sql-database-projects/src/sqldbproj.d.ts index 5bbb2f3d99..5ab5ba6869 100644 --- a/extensions/sql-database-projects/src/sqldbproj.d.ts +++ b/extensions/sql-database-projects/src/sqldbproj.d.ts @@ -149,18 +149,35 @@ declare module 'sqldbproj' { readProjFile(): Promise; /** - * Adds the list of sql files and directories to the project, and saves the project file - * - * @param list list of files and folder Uris. Files and folders must already exist. No files or folders will be added if any do not exist. + * Adds a pre-deployment script + * @param relativePath */ - addToProject(list: vscode.Uri[]): Promise; + addPreDeploymentScript(relativePath: string): Promise; + + /** + * Adds a post-deployment script + * @param relativePath + */ + addPostDeploymentScript(relativePath: string): Promise; + + /** + * Add a SQL object script that will be included in the schema + * @param relativePath + */ + addSqlObjectScript(relativePath: string): Promise; + + /** + * Adds multiple SQL object scripts that will be included in the schema + * @param relativePaths Array of paths relative to the .sqlproj file + */ + addSqlObjectScripts(relativePaths: string[]): Promise; /** * Adds a folder to the project, and saves the project file * * @param relativeFolderPath Relative path of the folder */ - addFolderItem(relativeFolderPath: string): Promise; + addFolder(relativeFolderPath: string): Promise; /** * Writes a file to disk if contents are provided, adds that file to the project, and writes it to disk @@ -194,18 +211,6 @@ declare module 'sqldbproj' { */ removeDatabaseSource(databaseSource: string): Promise; - /** - * Excludes entry from project by removing it from the project file - * @param entry - */ - exclude(entry: IFileProjectEntry): Promise; - - /** - * Deletes file or folder and removes it from the project file - * @param entry - */ - deleteFileFolder(entry: IFileProjectEntry): Promise; - /** * returns the sql version the project is targeting */ @@ -290,7 +295,7 @@ declare module 'sqldbproj' { * Represents a database reference entry in a project file */ export interface IDatabaseReferenceProjectEntry extends IFileProjectEntry { - databaseName: string; + referenceName: string; databaseVariableLiteralValue?: string; suppressMissingDependenciesErrors: boolean; } diff --git a/extensions/sql-database-projects/src/templates/templates.ts b/extensions/sql-database-projects/src/templates/templates.ts index 41ebb4032c..6903e1450b 100644 --- a/extensions/sql-database-projects/src/templates/templates.ts +++ b/extensions/sql-database-projects/src/templates/templates.ts @@ -77,7 +77,7 @@ export function macroExpansion(template: string, macroDict: Record { +async function loadObjectTypeInfo(key: ItemType, friendlyName: string, templateFolderPath: string, fileName: string): Promise { const template = await loadTemplate(templateFolderPath, fileName); scriptTypes.push(new ProjectScriptType(key, friendlyName, template)); @@ -89,11 +89,11 @@ async function loadTemplate(templateFolderPath: string, fileName: string): Promi } export class ProjectScriptType { - type: string; + type: ItemType; friendlyName: string; templateScript: string; - constructor(type: string, friendlyName: string, templateScript: string) { + constructor(type: ItemType, friendlyName: string, templateScript: string) { this.type = type; this.friendlyName = friendlyName; this.templateScript = templateScript; diff --git a/extensions/sql-database-projects/src/test/autorestHelper.test.ts b/extensions/sql-database-projects/src/test/autorestHelper.test.ts index ee0da6c8c4..4381f2ba21 100644 --- a/extensions/sql-database-projects/src/test/autorestHelper.test.ts +++ b/extensions/sql-database-projects/src/test/autorestHelper.test.ts @@ -25,7 +25,7 @@ describe('Autorest tests', function (): void { sinon.restore(); }); - after(async function(): Promise { + after(async function (): Promise { await testUtils.deleteGeneratedTestFolder(); }); @@ -41,7 +41,7 @@ describe('Autorest tests', function (): void { sinon.stub(window, 'showInformationMessage').returns(Promise.resolve(runViaNpx)); // stub a selection in case test runner doesn't have autorest installed const autorestHelper = new AutorestHelper(testContext.outputChannel); - const dummyFile = path.join(await testUtils.generateTestFolderPath(), 'testoutput.log'); + const dummyFile = path.join(await testUtils.generateTestFolderPath(this.test), 'testoutput.log'); sinon.stub(autorestHelper, 'constructAutorestCommand').returns(`${await autorestHelper.detectInstallation()} --version > ${dummyFile}`); try { diff --git a/extensions/sql-database-projects/src/test/baselines/SSDTProjectAfterUpdateBaseline.xml b/extensions/sql-database-projects/src/test/baselines/SSDTProjectAfterUpdateBaseline.xml deleted file mode 100644 index b1e76f1a7f..0000000000 --- a/extensions/sql-database-projects/src/test/baselines/SSDTProjectAfterUpdateBaseline.xml +++ /dev/null @@ -1,89 +0,0 @@ - - - - Debug - AnyCPU - TestProjectName - 2.0 - 4.1 - {BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575} - Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider - Database - - - TestProjectName - TestProjectName - 1033, CI - BySchemaAndSchemaType - True - v4.5 - CS - Properties - False - True - True - - - bin\Release\ - $(MSBuildProjectName).sql - False - pdbonly - true - false - true - prompt - 4 - - - bin\Debug\ - $(MSBuildProjectName).sql - false - true - full - false - true - true - prompt - 4 - - - 11.0 - - True - 11.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - False - master - - - False - master - - - diff --git a/extensions/sql-database-projects/src/test/baselines/SSDTProjectBaselineWithBeforeBuildTargetAfterUpdate.xml b/extensions/sql-database-projects/src/test/baselines/SSDTProjectBaselineWithBeforeBuildTargetAfterUpdate.xml deleted file mode 100644 index 605e89efc0..0000000000 --- a/extensions/sql-database-projects/src/test/baselines/SSDTProjectBaselineWithBeforeBuildTargetAfterUpdate.xml +++ /dev/null @@ -1,80 +0,0 @@ - - - - Debug - AnyCPU - TestProjectName - 2.0 - 4.1 - {BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575} - Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider - Database - - - TestProjectName - TestProjectName - 1033, CI - BySchemaAndSchemaType - True - v4.5 - CS - Properties - False - True - True - - - bin\Release\ - $(MSBuildProjectName).sql - False - pdbonly - true - false - true - prompt - 4 - - - bin\Debug\ - $(MSBuildProjectName).sql - false - true - full - false - true - true - prompt - 4 - - - 11.0 - - True - 11.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/extensions/sql-database-projects/src/test/baselines/SSDTUpdatedProjectAfterSystemDbUpdateBaseline.xml b/extensions/sql-database-projects/src/test/baselines/SSDTUpdatedProjectAfterSystemDbUpdateBaseline.xml deleted file mode 100644 index 22e6cada91..0000000000 --- a/extensions/sql-database-projects/src/test/baselines/SSDTUpdatedProjectAfterSystemDbUpdateBaseline.xml +++ /dev/null @@ -1,97 +0,0 @@ - - - - Debug - AnyCPU - TestProjectName - 2.0 - 4.1 - {BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575} - Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider - Database - - - TestProjectName - TestProjectName - 1033, CI - BySchemaAndSchemaType - True - v4.5 - CS - Properties - False - True - True - - - bin\Release\ - $(MSBuildProjectName).sql - False - pdbonly - true - false - true - prompt - 4 - - - bin\Debug\ - $(MSBuildProjectName).sql - false - true - full - false - true - true - prompt - 4 - - - 11.0 - - True - 11.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - False - master - - - False - master - - - False - msdb - - - False - msdb - - - diff --git a/extensions/sql-database-projects/src/test/baselines/baselines.ts b/extensions/sql-database-projects/src/test/baselines/baselines.ts index d97089338d..42a3a16c57 100644 --- a/extensions/sql-database-projects/src/test/baselines/baselines.ts +++ b/extensions/sql-database-projects/src/test/baselines/baselines.ts @@ -9,25 +9,16 @@ import { promises as fs } from 'fs'; // Project baselines export let newProjectFileBaseline: string; export let newProjectFileWithScriptBaseline: string; -export let newProjectFileNoPropertiesFolderBaseline: string; export let openProjectFileBaseline: string; -export let openProjectFileReleaseConfigurationBaseline: string; -export let openProjectFileUnknownConfigurationBaseline: string; -export let openProjectFileSingleOutputPathBaseline: string; -export let openProjectFileMultipleOutputPathBaseline: string; export let openDataSourcesBaseline: string; export let SSDTProjectFileBaseline: string; -export let SSDTProjectAfterUpdateBaseline: string; export let SSDTUpdatedProjectBaseline: string; -export let SSDTUpdatedProjectAfterSystemDbUpdateBaseline: string; export let SSDTProjectBaselineWithBeforeBuildTarget: string; -export let SSDTProjectBaselineWithBeforeBuildTargetAfterUpdate: string; export let publishProfileIntegratedSecurityBaseline: string; export let publishProfileSqlLoginBaseline: string; export let publishProfileDefaultValueBaseline: string; export let openProjectWithProjectReferencesBaseline: string; export let openSqlProjectWithPrePostDeploymentError: string; -export let openSqlProjectWithAdditionalSqlCmdVariablesBaseline: string; export let sqlProjectMissingVersionBaseline: string; export let sqlProjectInvalidVersionBaseline: string; export let sqlProjectCustomCollationBaseline: string; @@ -38,34 +29,24 @@ export let newStyleProjectSdkImportAttributeBaseline: string; export let openSdkStyleSqlProjectBaseline: string; export let openSdkStyleSqlProjectWithFilesSpecifiedBaseline: string; export let openSdkStyleSqlProjectWithGlobsSpecifiedBaseline: string; -export let openSdkStyleSqlProjectWithBuildRemoveBaseline: string; -export let openSdkStyleSqlProjectNoProjectGuidBaseline: string; -export let openSqlProjectWithAdditionalPublishProfileBaseline: string; +export let sqlProjPropertyReadBaseline: string; +export let databaseReferencesReadBaseline: string; const baselineFolderPath = __dirname; export async function loadBaselines() { newProjectFileBaseline = await loadBaseline(baselineFolderPath, 'newSqlProjectBaseline.xml'); newProjectFileWithScriptBaseline = await loadBaseline(baselineFolderPath, 'newSqlProjectWithScriptBaseline.xml'); - newProjectFileNoPropertiesFolderBaseline = await loadBaseline(baselineFolderPath, 'newSqlProjectNoPropertiesFolderBaseline.xml'); openProjectFileBaseline = await loadBaseline(baselineFolderPath, 'openSqlProjectBaseline.xml'); - openProjectFileReleaseConfigurationBaseline = await loadBaseline(baselineFolderPath, 'openSqlProjectReleaseConfigurationBaseline.xml'); - openProjectFileUnknownConfigurationBaseline = await loadBaseline(baselineFolderPath, 'openSqlProjectUnknownConfigurationBaseline.xml'); - openProjectFileSingleOutputPathBaseline = await loadBaseline(baselineFolderPath, 'openSqlProjectSingleOutputPathBaseline.xml'); - openProjectFileMultipleOutputPathBaseline = await loadBaseline(baselineFolderPath, 'openSqlProjectMultipleOutputPathBaseline.xml'); openDataSourcesBaseline = await loadBaseline(baselineFolderPath, 'openDataSourcesBaseline.json'); SSDTProjectFileBaseline = await loadBaseline(baselineFolderPath, 'SSDTProjectBaseline.xml'); - SSDTProjectAfterUpdateBaseline = await loadBaseline(baselineFolderPath, 'SSDTProjectAfterUpdateBaseline.xml'); SSDTUpdatedProjectBaseline = await loadBaseline(baselineFolderPath, 'SSDTUpdatedProjectBaseline.xml'); - SSDTUpdatedProjectAfterSystemDbUpdateBaseline = await loadBaseline(baselineFolderPath, 'SSDTUpdatedProjectAfterSystemDbUpdateBaseline.xml'); SSDTProjectBaselineWithBeforeBuildTarget = await loadBaseline(baselineFolderPath, 'SSDTProjectBaselineWithBeforeBuildTarget.xml'); - SSDTProjectBaselineWithBeforeBuildTargetAfterUpdate = await loadBaseline(baselineFolderPath, 'SSDTProjectBaselineWithBeforeBuildTargetAfterUpdate.xml'); publishProfileIntegratedSecurityBaseline = await loadBaseline(baselineFolderPath, 'publishProfileIntegratedSecurityBaseline.publish.xml'); publishProfileSqlLoginBaseline = await loadBaseline(baselineFolderPath, 'publishProfileSqlLoginBaseline.publish.xml'); publishProfileDefaultValueBaseline = await loadBaseline(baselineFolderPath, 'publishProfileDefaultValueBaseline.publish.xml'); openProjectWithProjectReferencesBaseline = await loadBaseline(baselineFolderPath, 'openSqlProjectWithProjectReferenceBaseline.xml'); openSqlProjectWithPrePostDeploymentError = await loadBaseline(baselineFolderPath, 'openSqlProjectWithPrePostDeploymentError.xml'); - openSqlProjectWithAdditionalSqlCmdVariablesBaseline = await loadBaseline(baselineFolderPath, 'openSqlProjectWithAdditionalSqlCmdVariablesBaseline.xml'); sqlProjectMissingVersionBaseline = await loadBaseline(baselineFolderPath, 'sqlProjectMissingVersionBaseline.xml'); sqlProjectInvalidVersionBaseline = await loadBaseline(baselineFolderPath, 'sqlProjectInvalidVersionBaseline.xml'); sqlProjectCustomCollationBaseline = await loadBaseline(baselineFolderPath, 'sqlProjectCustomCollationBaseline.xml'); @@ -76,9 +57,8 @@ export async function loadBaselines() { openSdkStyleSqlProjectBaseline = await loadBaseline(baselineFolderPath, 'openSdkStyleSqlProjectBaseline.xml'); openSdkStyleSqlProjectWithFilesSpecifiedBaseline = await loadBaseline(baselineFolderPath, 'openSdkStyleSqlProjectWithFilesSpecifiedBaseline.xml'); openSdkStyleSqlProjectWithGlobsSpecifiedBaseline = await loadBaseline(baselineFolderPath, 'openSdkStyleSqlProjectWithGlobsSpecifiedBaseline.xml'); - openSdkStyleSqlProjectWithBuildRemoveBaseline = await loadBaseline(baselineFolderPath, 'openSdkStyleSqlProjectWithBuildRemoveBaseline.xml'); - openSdkStyleSqlProjectNoProjectGuidBaseline = await loadBaseline(baselineFolderPath, 'openSdkStyleSqlProjectNoProjectGuidBaseline.xml'); - openSqlProjectWithAdditionalPublishProfileBaseline = await loadBaseline(baselineFolderPath, 'openSqlProjectWithAdditionalPublishProfileBaseline.xml'); + sqlProjPropertyReadBaseline = await loadBaseline(baselineFolderPath, 'sqlProjPropertyRead.xml'); + databaseReferencesReadBaseline = await loadBaseline(baselineFolderPath, 'databaseReferencesReadBaseline.xml'); } async function loadBaseline(baselineFolderPath: string, fileName: string): Promise { diff --git a/extensions/sql-database-projects/src/test/baselines/databaseReferencesReadBaseline.xml b/extensions/sql-database-projects/src/test/baselines/databaseReferencesReadBaseline.xml new file mode 100644 index 0000000000..a6adcb3628 --- /dev/null +++ b/extensions/sql-database-projects/src/test/baselines/databaseReferencesReadBaseline.xml @@ -0,0 +1,67 @@ + + + + + ReferenceTest + {843865B6-7286-4DA7-ADA3-BD5EA485B40A} + Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider + 1033, CI + + + + + + + True + msdbLiteral + + + True + msdbLiteral + + + False + dacpacDbVar + dacpacServerVar + + + True + OtherDacpacLiteral + + + + + projDbVar + $(SqlCmdVar__1) + + + projServerName + $(SqlCmdVar__2) + + + dacpacDbName + $(SqlCmdVar__3) + + + dacpacServerName + $(SqlCmdVar__4) + + + + + ReferencedProject + {BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575} + True + True + projDbVar + projServerVar + + + OtherProject + {C0DEBA11-BA5E-5EA7-ACE5-BABB1E70A575} + True + False + OtherProjLiteral + + + diff --git a/extensions/sql-database-projects/src/test/baselines/newSqlProjectNoPropertiesFolderBaseline.xml b/extensions/sql-database-projects/src/test/baselines/newSqlProjectNoPropertiesFolderBaseline.xml deleted file mode 100644 index 7e6eb2fa69..0000000000 --- a/extensions/sql-database-projects/src/test/baselines/newSqlProjectNoPropertiesFolderBaseline.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - Debug - AnyCPU - TestProjectName - 2.0 - 4.1 - {BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575} - Microsoft.Data.Tools.Schema.Sql.Sql160DatabaseSchemaProvider - Database - - - TestProjectName - TestProjectName - 1033, CI - BySchemaAndSchemaType - True - v4.5 - CS - Properties - False - True - True - - - bin\Release\ - $(MSBuildProjectName).sql - False - pdbonly - true - false - true - prompt - 4 - - - bin\Debug\ - $(MSBuildProjectName).sql - false - true - full - false - true - true - prompt - 4 - - - 11.0 - - True - 11.0 - - - - - - - - - - - diff --git a/extensions/sql-database-projects/src/test/baselines/openSdkStyleSqlProjectNoProjectGuidBaseline.xml b/extensions/sql-database-projects/src/test/baselines/openSdkStyleSqlProjectNoProjectGuidBaseline.xml deleted file mode 100644 index 5aa7dae678..0000000000 --- a/extensions/sql-database-projects/src/test/baselines/openSdkStyleSqlProjectNoProjectGuidBaseline.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - TestProjectName - Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider - 1033, CI - - - - - - - MyProdDatabase - $(SqlCmdVar__1) - - - MyBackupDatabase - $(SqlCmdVar__2) - - - - - - - diff --git a/extensions/sql-database-projects/src/test/baselines/openSdkStyleSqlProjectWithBuildRemoveBaseline.xml b/extensions/sql-database-projects/src/test/baselines/openSdkStyleSqlProjectWithBuildRemoveBaseline.xml deleted file mode 100644 index 89b608f780..0000000000 --- a/extensions/sql-database-projects/src/test/baselines/openSdkStyleSqlProjectWithBuildRemoveBaseline.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - TestProjectName - {2C283C5D-9E4A-4313-8FF9-4E0CEE20B063} - Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider - 1033, CI - - - - - - - - - - - - - - - - - - - - - False - master - - - False - master - - - diff --git a/extensions/sql-database-projects/src/test/baselines/openSqlProjectMultipleOutputPathBaseline.xml b/extensions/sql-database-projects/src/test/baselines/openSqlProjectMultipleOutputPathBaseline.xml deleted file mode 100644 index 78bcb8405a..0000000000 --- a/extensions/sql-database-projects/src/test/baselines/openSqlProjectMultipleOutputPathBaseline.xml +++ /dev/null @@ -1,114 +0,0 @@ - - - - Debug - AnyCPU - TestProjectName - 2.0 - 4.1 - {BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575} - Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider - Database - - - TestProjectName - TestProjectName - 1033, CI - BySchemaAndSchemaType - True - v4.5 - CS - Properties - False - True - True - - - bin\Release\ - $(MSBuildProjectName).sql - False - pdbonly - true - false - true - prompt - 4 - - - bin\Debug\ - $(MSBuildProjectName).sql - false - true - full - false - true - true - prompt - 4 - - - bin\other - - - 11.0 - - True - 11.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - MyProdDatabase - $(SqlCmdVar__1) - - - MyBackupDatabase - $(SqlCmdVar__2) - - - - - False - master - - - False - master - - - - - - - - - - - - - - diff --git a/extensions/sql-database-projects/src/test/baselines/openSqlProjectReleaseConfigurationBaseline.xml b/extensions/sql-database-projects/src/test/baselines/openSqlProjectReleaseConfigurationBaseline.xml deleted file mode 100644 index 19324c0b31..0000000000 --- a/extensions/sql-database-projects/src/test/baselines/openSqlProjectReleaseConfigurationBaseline.xml +++ /dev/null @@ -1,111 +0,0 @@ - - - - Release - AnyCPU - TestProjectName - 2.0 - 4.1 - {BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575} - Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider - Database - - - TestProjectName - TestProjectName - 1033, CI - BySchemaAndSchemaType - True - v4.5 - CS - Properties - False - True - True - - - bin\Release\ - $(MSBuildProjectName).sql - False - pdbonly - true - false - true - prompt - 4 - - - bin\Debug\ - $(MSBuildProjectName).sql - false - true - full - false - true - true - prompt - 4 - - - 11.0 - - True - 11.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - MyProdDatabase - $(SqlCmdVar__1) - - - MyBackupDatabase - $(SqlCmdVar__2) - - - - - False - master - - - False - master - - - - - - - - - - - - - - diff --git a/extensions/sql-database-projects/src/test/baselines/openSqlProjectSingleOutputPathBaseline.xml b/extensions/sql-database-projects/src/test/baselines/openSqlProjectSingleOutputPathBaseline.xml deleted file mode 100644 index a8587678d2..0000000000 --- a/extensions/sql-database-projects/src/test/baselines/openSqlProjectSingleOutputPathBaseline.xml +++ /dev/null @@ -1,89 +0,0 @@ - - - - Debug - AnyCPU - TestProjectName - 2.0 - 4.1 - {BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575} - Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider - Database - - - TestProjectName - TestProjectName - 1033, CI - BySchemaAndSchemaType - True - v4.5 - CS - Properties - False - True - True - ..\otherFolder - - - 11.0 - - True - 11.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - MyProdDatabase - $(SqlCmdVar__1) - - - MyBackupDatabase - $(SqlCmdVar__2) - - - - - False - master - - - False - master - - - - - - - - - - - - - - diff --git a/extensions/sql-database-projects/src/test/baselines/openSqlProjectUnknownConfigurationBaseline.xml b/extensions/sql-database-projects/src/test/baselines/openSqlProjectUnknownConfigurationBaseline.xml deleted file mode 100644 index 3bd55a2436..0000000000 --- a/extensions/sql-database-projects/src/test/baselines/openSqlProjectUnknownConfigurationBaseline.xml +++ /dev/null @@ -1,111 +0,0 @@ - - - - Unknown - AnyCPU - TestProjectName - 2.0 - 4.1 - {BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575} - Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider - Database - - - TestProjectName - TestProjectName - 1033, CI - BySchemaAndSchemaType - True - v4.5 - CS - Properties - False - True - True - - - bin\Release\ - $(MSBuildProjectName).sql - False - pdbonly - true - false - true - prompt - 4 - - - bin\Debug\ - $(MSBuildProjectName).sql - false - true - full - false - true - true - prompt - 4 - - - 11.0 - - True - 11.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - MyProdDatabase - $(SqlCmdVar__1) - - - MyBackupDatabase - $(SqlCmdVar__2) - - - - - False - master - - - False - master - - - - - - - - - - - - - - diff --git a/extensions/sql-database-projects/src/test/baselines/openSqlProjectWithAdditionalPublishProfileBaseline.xml b/extensions/sql-database-projects/src/test/baselines/openSqlProjectWithAdditionalPublishProfileBaseline.xml deleted file mode 100644 index 3c7e532944..0000000000 --- a/extensions/sql-database-projects/src/test/baselines/openSqlProjectWithAdditionalPublishProfileBaseline.xml +++ /dev/null @@ -1,117 +0,0 @@ - - - - Debug - AnyCPU - TestProjectName - 2.0 - 4.1 - {BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575} - Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider - Database - - - TestProjectName - TestProjectName - 1033, CI - BySchemaAndSchemaType - True - v4.5 - CS - Properties - False - True - True - - - bin\Release\ - $(MSBuildProjectName).sql - False - pdbonly - true - false - true - prompt - 4 - - - bin\Debug\ - $(MSBuildProjectName).sql - false - true - full - false - true - true - prompt - 4 - - - 11.0 - - True - 11.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - MyProdDatabase - $(SqlCmdVar__1) - - - MyBackupDatabase - $(SqlCmdVar__2) - - - - - False - master - - - False - master - - - - - - - - - - - - - - - - - - - - diff --git a/extensions/sql-database-projects/src/test/baselines/openSqlProjectWithAdditionalSqlCmdVariablesBaseline.xml b/extensions/sql-database-projects/src/test/baselines/openSqlProjectWithAdditionalSqlCmdVariablesBaseline.xml deleted file mode 100644 index 372a7cb90a..0000000000 --- a/extensions/sql-database-projects/src/test/baselines/openSqlProjectWithAdditionalSqlCmdVariablesBaseline.xml +++ /dev/null @@ -1,120 +0,0 @@ - - - - Debug - AnyCPU - TestProjectName - 2.0 - 4.1 - {BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575} - Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider - Database - - - TestProjectName - TestProjectName - 1033, CI - BySchemaAndSchemaType - True - v4.5 - CS - Properties - False - True - True - - - bin\Release\ - $(MSBuildProjectName).sql - False - pdbonly - true - false - true - prompt - 4 - - - bin\Debug\ - $(MSBuildProjectName).sql - false - true - full - false - true - true - prompt - 4 - - - 11.0 - - True - 11.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - MyBackupDatabase - $(SqlCmdVar__2) - - - TestDb - $(SqlCmdVar__3) - - - NewProdName - $(SqlCmdVar__4) - - - - - False - master - - - False - master - - - - - - - - - - - - - - - - - - - diff --git a/extensions/sql-database-projects/src/test/baselines/openSqlProjectWithProjectReferenceBaseline.xml b/extensions/sql-database-projects/src/test/baselines/openSqlProjectWithProjectReferenceBaseline.xml index 2ab7d4e2c6..c451bb6d92 100644 --- a/extensions/sql-database-projects/src/test/baselines/openSqlProjectWithProjectReferenceBaseline.xml +++ b/extensions/sql-database-projects/src/test/baselines/openSqlProjectWithProjectReferenceBaseline.xml @@ -95,12 +95,18 @@ - - TestProjectName + + MyReferencedTestDatabase + $(SqlCmdVar__1) + + + + + ReferencedTestProjectName {f9008554-068f-4f91-979a-58bd1f7c8f6e} True False - TestProjectName + ReferencedTestProjectName diff --git a/extensions/sql-database-projects/src/test/baselines/sqlProjPropertyRead.xml b/extensions/sql-database-projects/src/test/baselines/sqlProjPropertyRead.xml new file mode 100644 index 0000000000..c29c3beffe --- /dev/null +++ b/extensions/sql-database-projects/src/test/baselines/sqlProjPropertyRead.xml @@ -0,0 +1,18 @@ + + + + + SdkStyle + {BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575} + Microsoft.Data.Tools.Schema.Sql.Sql130DatabaseSchemaProvider + 1041, CI + Release + x64 + CustomOutputPath\Dacpacs\ + Japanese_CI_AS + oneSource;twoSource;redSource;blueSource + + + + + diff --git a/extensions/sql-database-projects/src/test/buildHelper.test.ts b/extensions/sql-database-projects/src/test/buildHelper.test.ts index 03f9932b4a..a918224414 100644 --- a/extensions/sql-database-projects/src/test/buildHelper.test.ts +++ b/extensions/sql-database-projects/src/test/buildHelper.test.ts @@ -9,26 +9,26 @@ import * as vscode from 'vscode'; import * as path from 'path'; import { BuildHelper } from '../tools/buildHelper'; import { TestContext, createContext } from './testContext'; +import { ProjectType } from 'mssql'; describe('BuildHelper: Build Helper tests', function (): void { - - it('Should get correct build arguments', function (): void { + it('Should get correct build arguments for legacy-style projects', function (): void { // update settings and validate const buildHelper = new BuildHelper(); - const resultArg = buildHelper.constructBuildArguments('dummy\\project path\\more space in path', 'dummy\\dll path', false); + const resultArg = buildHelper.constructBuildArguments('dummy\\project path\\more space in path', 'dummy\\dll path', ProjectType.LegacyStyle); if (os.platform() === 'win32') { - should(resultArg).equal(' build "dummy\\\\project path\\\\more space in path" /p:NetCoreBuild=true /p:NETCoreTargetsPath="dummy\\\\dll path"'); + should(resultArg).equal(' build "dummy\\\\project path\\\\more space in path" /p:NetCoreBuild=true /p:NETCoreTargetsPath="dummy\\\\dll path" /p:SystemDacpacsLocation="dummy\\\\dll path"'); } else { - should(resultArg).equal(' build "dummy/project path/more space in path" /p:NetCoreBuild=true /p:NETCoreTargetsPath="dummy/dll path"'); + should(resultArg).equal(' build "dummy/project path/more space in path" /p:NetCoreBuild=true /p:NETCoreTargetsPath="dummy/dll path" /p:SystemDacpacsLocation="dummy/dll path"'); } }); - it('Should get correct build arguments for sdk style projects', function (): void { + it('Should get correct build arguments for SDK-style projects', function (): void { // update settings and validate const buildHelper = new BuildHelper(); - const resultArg = buildHelper.constructBuildArguments('dummy\\project path\\more space in path', 'dummy\\dll path', true); + const resultArg = buildHelper.constructBuildArguments('dummy\\project path\\more space in path', 'dummy\\dll path', ProjectType.SdkStyle); if (os.platform() === 'win32') { should(resultArg).equal(' build "dummy\\\\project path\\\\more space in path" /p:NetCoreBuild=true /p:SystemDacpacsLocation="dummy\\\\dll path"'); diff --git a/extensions/sql-database-projects/src/test/datasource.test.ts b/extensions/sql-database-projects/src/test/datasource.test.ts index c0ec91258a..7125d179bc 100644 --- a/extensions/sql-database-projects/src/test/datasource.test.ts +++ b/extensions/sql-database-projects/src/test/datasource.test.ts @@ -10,16 +10,16 @@ import * as sql from '../models/dataSources/sqlConnectionStringSource'; import * as dataSources from '../models/dataSources/dataSources'; describe('Data Sources: DataSource operations', function (): void { - before(async function () : Promise { + before(async function (): Promise { await baselines.loadBaselines(); }); - after(async function(): Promise { + after(async function (): Promise { await testUtils.deleteGeneratedTestFolder(); }); it.skip('Should read DataSources from datasource.json', async function (): Promise { - const dataSourcePath = await testUtils.createTestDataSources(baselines.openDataSourcesBaseline); + const dataSourcePath = await testUtils.createTestDataSources(this.test, baselines.openDataSourcesBaseline); const dataSourceList = await dataSources.load(dataSourcePath); should(dataSourceList.length).equal(3); @@ -36,7 +36,7 @@ describe('Data Sources: DataSource operations', function (): void { should((dataSourceList[2] as sql.SqlConnectionDataSource).azureMFA).equal(true); }); - it ('Should be able to create sql data source from connection strings with and without ending semicolon', function (): void { + it('Should be able to create sql data source from connection strings with and without ending semicolon', function (): void { should.doesNotThrow(() => new sql.SqlConnectionDataSource('no ending semicolon', 'Data Source=(LOCAL);Initial Catalog=testdb;User id=sa;Password=PLACEHOLDER')); should.doesNotThrow(() => new sql.SqlConnectionDataSource('ending in semicolon', 'Data Source=(LOCAL);Initial Catalog=testdb;User id=sa;Password=PLACEHOLDER;')); should.throws(() => new sql.SqlConnectionDataSource('invalid extra equals sign', 'Data Source=(LOCAL);Initial Catalog=testdb=extra;User id=sa;Password=PLACEHOLDER')); diff --git a/extensions/sql-database-projects/src/test/deploy/deployService.test.ts b/extensions/sql-database-projects/src/test/deploy/deployService.test.ts index 7b18b7378d..9fc3158c01 100644 --- a/extensions/sql-database-projects/src/test/deploy/deployService.test.ts +++ b/extensions/sql-database-projects/src/test/deploy/deployService.test.ts @@ -70,7 +70,7 @@ describe('deploy service', function (): void { sandbox = sinon.createSandbox(); }); - after(async function(): Promise { + after(async function (): Promise { await testUtils.deleteGeneratedTestFolder(); }); @@ -93,7 +93,7 @@ describe('deploy service', function (): void { dockerBaseImageEula: '' } }; - const projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.newProjectFileBaseline); const project1 = await Project.openProject(vscode.Uri.file(projFilePath).fsPath); const shellExecutionHelper = TypeMoq.Mock.ofType(ShellExecutionHelper); shellExecutionHelper.setup(x => x.runStreamedCommand(TypeMoq.It.isAny(), @@ -128,7 +128,7 @@ describe('deploy service', function (): void { dockerBaseImageEula: '' } }; - const projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.newProjectFileBaseline); const project1 = await Project.openProject(vscode.Uri.file(projFilePath).fsPath); const shellExecutionHelper = TypeMoq.Mock.ofType(ShellExecutionHelper); shellExecutionHelper.setup(x => x.runStreamedCommand(TypeMoq.It.isAny(), diff --git a/extensions/sql-database-projects/src/test/dialogs/addDatabaseReferenceDialog.test.ts b/extensions/sql-database-projects/src/test/dialogs/addDatabaseReferenceDialog.test.ts index 78b96ac710..955bb71005 100644 --- a/extensions/sql-database-projects/src/test/dialogs/addDatabaseReferenceDialog.test.ts +++ b/extensions/sql-database-projects/src/test/dialogs/addDatabaseReferenceDialog.test.ts @@ -22,28 +22,38 @@ describe('Add Database Reference Dialog', () => { }); beforeEach(function (): void { - const dataWorkspaceMock = TypeMoq.Mock.ofType(); - dataWorkspaceMock.setup(x => x.getProjectsInWorkspace(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve([])); - sinon.stub(vscode.extensions, 'getExtension').returns({ exports: dataWorkspaceMock.object }); + // const dataWorkspaceMock = TypeMoq.Mock.ofType(); + // dataWorkspaceMock.setup(x => x.getProjectsInWorkspace(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve([])); + // sinon.stub(vscode.extensions, 'getExtension').withArgs('Microsoft.data-workspace').returns({ exports: dataWorkspaceMock.object }); }); afterEach(function (): void { sinon.restore(); }); - after(async function(): Promise { + after(async function (): Promise { await testUtils.deleteGeneratedTestFolder(); }); it('Should open dialog successfully', async function (): Promise { - const project = await testUtils.createTestProject(baselines.newProjectFileBaseline); + const project = await testUtils.createTestProject(this.test, baselines.newProjectFileBaseline); + + const dataWorkspaceMock = TypeMoq.Mock.ofType(); + dataWorkspaceMock.setup(x => x.getProjectsInWorkspace(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve([])); + sinon.stub(vscode.extensions, 'getExtension').withArgs('Microsoft.data-workspace').returns({ exports: dataWorkspaceMock.object }); + const dialog = new AddDatabaseReferenceDialog(project); await dialog.openDialog(); should.notEqual(dialog.addDatabaseReferenceTab, undefined); }); it('Should enable ok button correctly', async function (): Promise { - const project = await testUtils.createTestProject(baselines.newProjectFileBaseline); + const project = await testUtils.createTestProject(this.test, baselines.newProjectFileBaseline); + + const dataWorkspaceMock = TypeMoq.Mock.ofType(); + dataWorkspaceMock.setup(x => x.getProjectsInWorkspace(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve([])); + sinon.stub(vscode.extensions, 'getExtension').withArgs('Microsoft.data-workspace').returns({ exports: dataWorkspaceMock.object }); + const dialog = new AddDatabaseReferenceDialog(project); await dialog.openDialog(); @@ -97,35 +107,40 @@ describe('Add Database Reference Dialog', () => { }); it('Should enable and disable input boxes depending on the reference type', async function (): Promise { - const project = await testUtils.createTestProject(baselines.newProjectFileBaseline); + const project = await testUtils.createTestProject(this.test, baselines.newProjectFileBaseline); + + const dataWorkspaceMock = TypeMoq.Mock.ofType(); + dataWorkspaceMock.setup(x => x.getProjectsInWorkspace(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve([])); + sinon.stub(vscode.extensions, 'getExtension').withArgs('Microsoft.data-workspace').returns({ exports: dataWorkspaceMock.object }); + const dialog = new AddDatabaseReferenceDialog(project); await dialog.openDialog(); // dialog starts with system db because there aren't any other projects in the workspace should(dialog.currentReferenceType).equal(ReferenceType.systemDb); - validateInputBoxEnabledStates(dialog, { databaseNameEnabled: true, databaseVariableEnabled: false, serverNameEnabled: false, serverVariabledEnabled: false}); + validateInputBoxEnabledStates(dialog, { databaseNameEnabled: true, databaseVariableEnabled: false, serverNameEnabled: false, serverVariabledEnabled: false }); // change to dacpac reference dialog.dacpacRadioButtonClick(); should(dialog.currentReferenceType).equal(ReferenceType.dacpac); should(dialog.locationDropdown!.value).equal(constants.differentDbSameServer); - validateInputBoxEnabledStates(dialog, { databaseNameEnabled: true, databaseVariableEnabled: true, serverNameEnabled: false, serverVariabledEnabled: false}); + validateInputBoxEnabledStates(dialog, { databaseNameEnabled: true, databaseVariableEnabled: true, serverNameEnabled: false, serverVariabledEnabled: false }); // change location to different db, different server dialog.locationDropdown!.value = constants.differentDbDifferentServer; dialog.updateEnabledInputBoxes(); - validateInputBoxEnabledStates(dialog, { databaseNameEnabled: true, databaseVariableEnabled: true, serverNameEnabled: true, serverVariabledEnabled: true}); + validateInputBoxEnabledStates(dialog, { databaseNameEnabled: true, databaseVariableEnabled: true, serverNameEnabled: true, serverVariabledEnabled: true }); // change location to same db dialog.locationDropdown!.value = constants.sameDatabase; 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}); + validateInputBoxEnabledStates(dialog, { databaseNameEnabled: false, databaseVariableEnabled: false, serverNameEnabled: false, serverVariabledEnabled: false }); }); }); @@ -139,6 +154,6 @@ interface inputBoxExpectedStates { function validateInputBoxEnabledStates(dialog: AddDatabaseReferenceDialog, expectedStates: inputBoxExpectedStates): void { should(dialog.databaseNameTextbox?.enabled).equal(expectedStates.databaseNameEnabled, `Database name text box should be ${expectedStates.databaseNameEnabled}. Actual: ${dialog.databaseNameTextbox?.enabled}`); should(dialog.databaseVariableTextbox?.enabled).equal(expectedStates.databaseVariableEnabled, `Database variable text box should be ${expectedStates.databaseVariableEnabled}. Actual: ${dialog.databaseVariableTextbox?.enabled}`); - should(dialog.serverNameTextbox?.enabled).equal(expectedStates.serverNameEnabled, `Server name text box should be ${expectedStates.serverNameEnabled}. Actual: ${dialog.serverNameTextbox?.enabled}`); + should(dialog.serverNameTextbox?.enabled).equal(expectedStates.serverNameEnabled, `Server name text box should be ${expectedStates.serverNameEnabled}. Actual: ${dialog.serverNameTextbox?.enabled}`); should(dialog.serverVariableTextbox?.enabled).equal(expectedStates.serverVariabledEnabled, `Server variable text box should be ${expectedStates.serverVariabledEnabled}. Actual: ${dialog.serverVariableTextbox?.enabled}`); } diff --git a/extensions/sql-database-projects/src/test/dialogs/createProjectFromDatabaseQuickpick.test.ts b/extensions/sql-database-projects/src/test/dialogs/createProjectFromDatabaseQuickpick.test.ts index 04824c21fa..618ef79863 100644 --- a/extensions/sql-database-projects/src/test/dialogs/createProjectFromDatabaseQuickpick.test.ts +++ b/extensions/sql-database-projects/src/test/dialogs/createProjectFromDatabaseQuickpick.test.ts @@ -155,9 +155,9 @@ describe('Create Project From Database Quickpick', () => { it('Should exit when folder structure is not selected and existing folder/file location is selected', async function (): Promise { //create folder and project file const projectFileName = 'TestProject'; - const testProjectFilePath = await generateTestFolderPath(); + const testProjectFilePath = await generateTestFolderPath(this.test); await fs.rm(testProjectFilePath, { force: true, recursive: true }); //clean up if it already exists - await createTestFile('', `${projectFileName}.sqlproj`, testProjectFilePath); + await createTestFile(this.test, '', `${projectFileName}.sqlproj`, testProjectFilePath); //user chooses connection and database sinon.stub(testUtils.vscodeMssqlIExtension.object, 'connect').resolves('testConnectionURI'); diff --git a/extensions/sql-database-projects/src/test/dialogs/publishDatabaseDialog.test.ts b/extensions/sql-database-projects/src/test/dialogs/publishDatabaseDialog.test.ts index 65377845ba..a2ba3e3dfa 100644 --- a/extensions/sql-database-projects/src/test/dialogs/publishDatabaseDialog.test.ts +++ b/extensions/sql-database-projects/src/test/dialogs/publishDatabaseDialog.test.ts @@ -26,13 +26,13 @@ describe('Publish Database Dialog', () => { testContext = createContext(); }); - after(async function(): Promise { + after(async function (): Promise { await testUtils.deleteGeneratedTestFolder(); }); it('Should open dialog successfully ', async function (): Promise { const projController = new ProjectsController(testContext.outputChannel); - const projFileDir = path.join(testUtils.generateBaseFolderName(), `TestProject_${new Date().getTime()}`); + const projFileDir = await testUtils.generateTestFolderPath(this.test); const projFilePath = await projController.createNewProject({ newProjName: 'TestProjectName', @@ -50,8 +50,7 @@ describe('Publish Database Dialog', () => { it('Should create default database name correctly ', async function (): Promise { const projController = new ProjectsController(testContext.outputChannel); - const projFolder = `TestProject_${new Date().getTime()}`; - const projFileDir = path.join(testUtils.generateBaseFolderName(), projFolder); + const projFileDir = await testUtils.generateTestFolderPath(this.test); const projFilePath = await projController.createNewProject({ newProjName: 'TestProjectName', @@ -68,7 +67,7 @@ describe('Publish Database Dialog', () => { }); it('Should include all info in publish profile', async function (): Promise { - const proj = await testUtils.createTestProject(baselines.openProjectFileBaseline); + const proj = await testUtils.createTestProject(this.test, baselines.openProjectFileBaseline); const dialog = TypeMoq.Mock.ofType(PublishDatabaseDialog, undefined, undefined, proj); dialog.setup(x => x.getConnectionUri()).returns(() => { return Promise.resolve('Mock|Connection|Uri'); }); dialog.setup(x => x.targetDatabaseName).returns(() => 'MockDatabaseName'); diff --git a/extensions/sql-database-projects/src/test/dialogs/publishOptionsDialog.test.ts b/extensions/sql-database-projects/src/test/dialogs/publishOptionsDialog.test.ts index c267e10a38..d883eac5fd 100644 --- a/extensions/sql-database-projects/src/test/dialogs/publishOptionsDialog.test.ts +++ b/extensions/sql-database-projects/src/test/dialogs/publishOptionsDialog.test.ts @@ -19,7 +19,7 @@ describe('Publish Database Options Dialog', () => { await baselines.loadBaselines(); }); - after(async function(): Promise { + after(async function (): Promise { await testUtils.deleteGeneratedTestFolder(); }); @@ -36,7 +36,7 @@ describe('Publish Database Options Dialog', () => { it('Should deployment options gets initialized correctly with sample test project', async function (): Promise { // Create new sample test project - const project = await testUtils.createTestProject(baselines.openProjectFileBaseline); + const project = await testUtils.createTestProject(this.test, baselines.openProjectFileBaseline); const dialog = TypeMoq.Mock.ofType(PublishDatabaseDialog, undefined, undefined, project); dialog.setup(x => x.getDeploymentOptions()).returns(() => { return Promise.resolve(testData.mockDacFxOptionsResult.deploymentOptions); }); diff --git a/extensions/sql-database-projects/src/test/dialogs/updateProjectFromDatabaseDialog.test.ts b/extensions/sql-database-projects/src/test/dialogs/updateProjectFromDatabaseDialog.test.ts index 462af2a062..204b3c79d3 100644 --- a/extensions/sql-database-projects/src/test/dialogs/updateProjectFromDatabaseDialog.test.ts +++ b/extensions/sql-database-projects/src/test/dialogs/updateProjectFromDatabaseDialog.test.ts @@ -36,7 +36,7 @@ describe('Update Project From Database Dialog', () => { }); it('Should populate endpoints correctly when Project context is passed', async function (): Promise { - const project = await testUtils.createTestProject(baselines.openProjectFileBaseline); + const project = await testUtils.createTestProject(this.test, baselines.openProjectFileBaseline); const dialog = new UpdateProjectFromDatabaseDialog(undefined, project, mockURIList); await dialog.openDialog(); @@ -62,7 +62,7 @@ describe('Update Project From Database Dialog', () => { }); it('Should populate endpoints correctly when context is complete', async function (): Promise { - const project = await testUtils.createTestProject(baselines.openProjectFileBaseline); + const project = await testUtils.createTestProject(this.test, baselines.openProjectFileBaseline); sinon.stub(azdata.connection, 'getConnections').resolves([mockConnectionProfile]); sinon.stub(azdata.connection, 'listDatabases').resolves([mockConnectionProfile.databaseName!]); diff --git a/extensions/sql-database-projects/src/test/netCoreTool.test.ts b/extensions/sql-database-projects/src/test/netCoreTool.test.ts index 780a22351a..8d5e9ac671 100644 --- a/extensions/sql-database-projects/src/test/netCoreTool.test.ts +++ b/extensions/sql-database-projects/src/test/netCoreTool.test.ts @@ -25,7 +25,7 @@ describe('NetCoreTool: Net core tests', function (): void { testContext = createContext(); }); - after(async function(): Promise { + after(async function (): Promise { await deleteGeneratedTestFolder(); }); @@ -55,7 +55,7 @@ describe('NetCoreTool: Net core tests', function (): void { should(result).true('dotnet not present in programfiles by default'); } - if (os.platform() === 'linux'){ + if (os.platform() === 'linux') { //check that path should start with /usr/share let result = !netcoreTool.netcoreInstallLocation || netcoreTool.netcoreInstallLocation.toLowerCase() === '/usr/share/dotnet'; should(result).true('dotnet not present in /usr/share'); @@ -70,7 +70,7 @@ describe('NetCoreTool: Net core tests', function (): void { it('should run a command successfully', async function (): Promise { const netcoreTool = new NetCoreTool(testContext.outputChannel); - const dummyFile = path.join(await generateTestFolderPath(), 'dummy.dacpac'); + const dummyFile = path.join(await generateTestFolderPath(this.test), 'dummy.dacpac'); try { await netcoreTool.runStreamedCommand('echo test > ' + getQuotedPath(dummyFile), undefined); diff --git a/extensions/sql-database-projects/src/test/newProjectTool.test.ts b/extensions/sql-database-projects/src/test/newProjectTool.test.ts index 3e9ac4c26d..c5d8f62525 100644 --- a/extensions/sql-database-projects/src/test/newProjectTool.test.ts +++ b/extensions/sql-database-projects/src/test/newProjectTool.test.ts @@ -11,25 +11,25 @@ import * as dataworkspace from 'dataworkspace'; import * as newProjectTool from '../tools/newProjectTool'; import { generateTestFolderPath, createTestFile, deleteGeneratedTestFolder } from './testUtils'; -let previousSetting : string; -let testFolderPath : string; +let previousSetting: string; +let testFolderPath: string; describe('NewProjectTool: New project tool tests', function (): void { const projectConfigurationKey = 'projects'; - const projectSaveLocationKey= 'defaultProjectSaveLocation'; + const projectSaveLocationKey = 'defaultProjectSaveLocation'; beforeEach(async function () { previousSetting = await vscode.workspace.getConfiguration(projectConfigurationKey)[projectSaveLocationKey]; - testFolderPath = await generateTestFolderPath(); + testFolderPath = await generateTestFolderPath(this.test); // set the default project folder path to the test folder await vscode.workspace.getConfiguration(projectConfigurationKey).update(projectSaveLocationKey, testFolderPath, true); const dataWorkspaceMock = TypeMoq.Mock.ofType(); dataWorkspaceMock.setup(x => x.defaultProjectSaveLocation).returns(() => vscode.Uri.file(testFolderPath)); - sinon.stub(vscode.extensions, 'getExtension').returns({ exports: dataWorkspaceMock.object}); + sinon.stub(vscode.extensions, 'getExtension').returns({ exports: dataWorkspaceMock.object }); }); - after(async function(): Promise { + after(async function (): Promise { await deleteGeneratedTestFolder(); }); @@ -47,20 +47,20 @@ describe('NewProjectTool: New project tool tests', function (): void { it('Should auto-increment default project names for new projects', async function (): Promise { should(newProjectTool.defaultProjectNameNewProj()).equal('DatabaseProject1'); - await createTestFile('', 'DatabaseProject1', testFolderPath); + await createTestFile(this.test, '', 'DatabaseProject1', testFolderPath); should(newProjectTool.defaultProjectNameNewProj()).equal('DatabaseProject2'); - await createTestFile('', 'DatabaseProject2', testFolderPath); + await createTestFile(this.test, '', 'DatabaseProject2', testFolderPath); should(newProjectTool.defaultProjectNameNewProj()).equal('DatabaseProject3'); }); it('Should auto-increment default project names for create project for database', async function (): Promise { should(newProjectTool.defaultProjectNameFromDb('master')).equal('DatabaseProjectmaster'); - await createTestFile('', 'DatabaseProjectmaster', testFolderPath); + await createTestFile(this.test, '', 'DatabaseProjectmaster', testFolderPath); should(newProjectTool.defaultProjectNameFromDb('master')).equal('DatabaseProjectmaster2'); - await createTestFile('', 'DatabaseProjectmaster2', testFolderPath); + await createTestFile(this.test, '', 'DatabaseProjectmaster2', testFolderPath); should(newProjectTool.defaultProjectNameFromDb('master')).equal('DatabaseProjectmaster3'); }); diff --git a/extensions/sql-database-projects/src/test/project.test.ts b/extensions/sql-database-projects/src/test/project.test.ts index 67dbd443aa..11601ffad3 100644 --- a/extensions/sql-database-projects/src/test/project.test.ts +++ b/extensions/sql-database-projects/src/test/project.test.ts @@ -5,7 +5,6 @@ import * as should from 'should'; import * as path from 'path'; -import * as os from 'os'; import * as sinon from 'sinon'; import * as baselines from './baselines/baselines'; import * as testUtils from './testUtils'; @@ -13,39 +12,40 @@ import * as constants from '../common/constants'; import { promises as fs } from 'fs'; import { Project } from '../models/project'; -import { exists, convertSlashesForSqlProj, getWellKnownDatabaseSources, getPlatformSafeFileEntryPath } from '../common/utils'; +import { exists, convertSlashesForSqlProj, getPlatformSafeFileEntryPath, systemDatabaseToString } from '../common/utils'; import { Uri, window } from 'vscode'; import { IDacpacReferenceSettings, IProjectReferenceSettings, ISystemDatabaseReferenceSettings } from '../models/IDatabaseReferenceSettings'; -import { EntryType, ItemType, SqlTargetPlatform } from 'sqldbproj'; -import { SystemDatabaseReferenceProjectEntry, SqlProjectReferenceProjectEntry, SystemDatabase } from '../models/projectEntry'; - -let projFilePath: string; +import { ItemType } from 'sqldbproj'; +import { SystemDatabaseReferenceProjectEntry, SqlProjectReferenceProjectEntry, DacpacReferenceProjectEntry } from '../models/projectEntry'; +import { ProjectType, SystemDatabase } from 'mssql'; describe('Project: sqlproj content operations', function (): void { before(async function (): Promise { await baselines.loadBaselines(); }); - beforeEach(async () => { - projFilePath = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline); - }); - after(async function (): Promise { await testUtils.deleteGeneratedTestFolder(); }); it('Should read Project from sqlproj', async function (): Promise { + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.openProjectFileBaseline); const project: Project = await Project.openProject(projFilePath); // Files and folders - should(project.files.filter(f => f.type === EntryType.File).length).equal(6); - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(4); - - should(project.files.find(f => f.type === EntryType.Folder && f.relativePath === 'Views\\User\\')).not.equal(undefined); // mixed ItemGroup folder - should(project.files.find(f => f.type === EntryType.File && f.relativePath === 'Views\\User\\Profile.sql')).not.equal(undefined); // mixed ItemGroup file - should(project.files.find(f => f.type === EntryType.File && f.relativePath === '..\\Test\\Test.sql')).not.equal(undefined); // mixed ItemGroup file - should(project.files.find(f => f.type === EntryType.File && f.relativePath === 'MyExternalStreamingJob.sql')).not.equal(undefined); // entry with custom attribute + (project.files.map(f => f.relativePath)).should.deepEqual([ + '..\\Test\\Test.sql', + 'MyExternalStreamingJob.sql', + 'Tables\\Action History.sql', + 'Tables\\Users.sql', + 'Views\\Maintenance\\Database Performance.sql', + 'Views\\User\\Profile.sql']); + (project.folders.map(f => f.relativePath)).should.deepEqual([ + 'Tables', + 'Views', + 'Views\\Maintenance', + 'Views\\User']); // SqlCmdVariables should(Object.keys(project.sqlCmdVariables).length).equal(2); @@ -55,42 +55,42 @@ describe('Project: sqlproj content operations', function (): void { // Database references // should only have one database reference even though there are two master.dacpac references (1 for ADS and 1 for SSDT) should(project.databaseReferences.length).equal(1); - should(project.databaseReferences[0].databaseName).containEql(constants.master); + should(project.databaseReferences[0].referenceName).containEql(constants.master); should(project.databaseReferences[0] instanceof SystemDatabaseReferenceProjectEntry).equal(true); // Pre-post deployment scripts should(project.preDeployScripts.length).equal(1); should(project.postDeployScripts.length).equal(1); should(project.noneDeployScripts.length).equal(2); - should(project.preDeployScripts.find(f => f.type === EntryType.File && f.relativePath === 'Script.PreDeployment1.sql')).not.equal(undefined, 'File Script.PreDeployment1.sql not read'); - should(project.postDeployScripts.find(f => f.type === EntryType.File && f.relativePath === 'Script.PostDeployment1.sql')).not.equal(undefined, 'File Script.PostDeployment1.sql not read'); - should(project.noneDeployScripts.find(f => f.type === EntryType.File && f.relativePath === 'Script.PreDeployment2.sql')).not.equal(undefined, 'File Script.PostDeployment2.sql not read'); - should(project.noneDeployScripts.find(f => f.type === EntryType.File && f.relativePath === 'Tables\\Script.PostDeployment1.sql')).not.equal(undefined, 'File Tables\\Script.PostDeployment1.sql not read'); + should(project.preDeployScripts.find(f => f.relativePath === 'Script.PreDeployment1.sql')).not.equal(undefined, 'File Script.PreDeployment1.sql not read'); + should(project.postDeployScripts.find(f => f.relativePath === 'Script.PostDeployment1.sql')).not.equal(undefined, 'File Script.PostDeployment1.sql not read'); + should(project.noneDeployScripts.find(f => f.relativePath === 'Script.PreDeployment2.sql')).not.equal(undefined, 'File Script.PostDeployment2.sql not read'); + should(project.noneDeployScripts.find(f => f.relativePath === 'Tables\\Script.PostDeployment1.sql')).not.equal(undefined, 'File Tables\\Script.PostDeployment1.sql not read'); // Publish profiles should(project.publishProfiles.length).equal(3); - should(project.publishProfiles.find(f => f.type === EntryType.File && f.relativePath === 'TestProjectName_1.publish.xml')).not.equal(undefined, 'Profile TestProjectName_1.publish.xml not read'); - should(project.publishProfiles.find(f => f.type === EntryType.File && f.relativePath === 'TestProjectName_2.publish.xml')).not.equal(undefined, 'Profile TestProjectName_2.publish.xml not read'); - should(project.publishProfiles.find(f => f.type === EntryType.File && f.relativePath === 'TestProjectName_3.publish.xml')).not.equal(undefined, 'Profile TestProjectName_3.publish.xml not read'); + should(project.publishProfiles.find(f => f.relativePath === 'TestProjectName_1.publish.xml')).not.equal(undefined, 'Profile TestProjectName_1.publish.xml not read'); + should(project.publishProfiles.find(f => f.relativePath === 'TestProjectName_2.publish.xml')).not.equal(undefined, 'Profile TestProjectName_2.publish.xml not read'); + should(project.publishProfiles.find(f => f.relativePath === 'TestProjectName_3.publish.xml')).not.equal(undefined, 'Profile TestProjectName_3.publish.xml not read'); }); it('Should read Project with Project reference from sqlproj', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.openProjectWithProjectReferencesBaseline); + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.openProjectWithProjectReferencesBaseline); const project: Project = await Project.openProject(projFilePath); // Database references // should only have two database references even though there are two master.dacpac references (1 for ADS and 1 for SSDT) - should(project.databaseReferences.length).equal(2); - should(project.databaseReferences[0].databaseName).containEql(constants.master); - should(project.databaseReferences[0] instanceof SystemDatabaseReferenceProjectEntry).equal(true); - should(project.databaseReferences[1].databaseName).containEql('TestProjectName'); - should(project.databaseReferences[1] instanceof SqlProjectReferenceProjectEntry).equal(true); + (project.databaseReferences.length).should.equal(2); + (project.databaseReferences[0].referenceName).should.containEql('ReferencedTestProject'); + (project.databaseReferences[0] instanceof SqlProjectReferenceProjectEntry).should.be.true(); + (project.databaseReferences[1].referenceName).should.containEql(constants.master); + (project.databaseReferences[1] instanceof SystemDatabaseReferenceProjectEntry).should.be.true(); }); it('Should throw warning message while reading Project with more than 1 pre-deploy script from sqlproj', async function (): Promise { const stub = sinon.stub(window, 'showWarningMessage').returns(Promise.resolve(constants.okString)); - projFilePath = await testUtils.createTestSqlProjFile(baselines.openSqlProjectWithPrePostDeploymentError); + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.openSqlProjectWithPrePostDeploymentError); const project: Project = await Project.openProject(projFilePath); should(stub.calledOnce).be.true('showWarningMessage should have been called exactly once'); @@ -99,569 +99,145 @@ describe('Project: sqlproj content operations', function (): void { should(project.preDeployScripts.length).equal(2); should(project.postDeployScripts.length).equal(1); should(project.noneDeployScripts.length).equal(1); - should(project.preDeployScripts.find(f => f.type === EntryType.File && f.relativePath === 'Script.PreDeployment1.sql')).not.equal(undefined, 'File Script.PreDeployment1.sql not read'); - should(project.postDeployScripts.find(f => f.type === EntryType.File && f.relativePath === 'Script.PostDeployment1.sql')).not.equal(undefined, 'File Script.PostDeployment1.sql not read'); - should(project.preDeployScripts.find(f => f.type === EntryType.File && f.relativePath === 'Script.PreDeployment2.sql')).not.equal(undefined, 'File Script.PostDeployment2.sql not read'); - should(project.noneDeployScripts.find(f => f.type === EntryType.File && f.relativePath === 'Tables\\Script.PostDeployment1.sql')).not.equal(undefined, 'File Tables\\Script.PostDeployment1.sql not read'); + should(project.preDeployScripts.find(f => f.relativePath === 'Script.PreDeployment1.sql')).not.equal(undefined, 'File Script.PreDeployment1.sql not read'); + should(project.postDeployScripts.find(f => f.relativePath === 'Script.PostDeployment1.sql')).not.equal(undefined, 'File Script.PostDeployment1.sql not read'); + should(project.preDeployScripts.find(f => f.relativePath === 'Script.PreDeployment2.sql')).not.equal(undefined, 'File Script.PostDeployment2.sql not read'); + should(project.noneDeployScripts.find(f => f.relativePath === 'Tables\\Script.PostDeployment1.sql')).not.equal(undefined, 'File Tables\\Script.PostDeployment1.sql not read'); sinon.restore(); }); - it('Should add Folder and Build entries to sqlproj', async function (): Promise { - const project = await Project.openProject(projFilePath); + it('Should perform Folder and SQL object script operations', async function (): Promise { + const project = await testUtils.createTestSqlProject(this.test); - const folderPath = 'Stored Procedures\\'; + const folderPath = 'Stored Procedures'; const scriptPath = path.join(folderPath, 'Fake Stored Proc.sql'); const scriptContents = 'SELECT \'This is not actually a stored procedure.\''; const scriptPathTagged = path.join(folderPath, 'Fake External Streaming Job.sql'); const scriptContentsTagged = 'EXEC sys.sp_create_streaming_job \'job\', \'SELECT 7\''; - await project.addFolderItem(folderPath); + (project.folders.length).should.equal(0); + (project.files.length).should.equal(0); + + await project.addFolder(folderPath); await project.addScriptItem(scriptPath, scriptContents); await project.addScriptItem(scriptPathTagged, scriptContentsTagged, ItemType.externalStreamingJob); - const newProject = await Project.openProject(projFilePath); + (project.folders.length).should.equal(1); + (project.files.length).should.equal(2); - should(newProject.files.find(f => f.type === EntryType.Folder && f.relativePath === convertSlashesForSqlProj(folderPath))).not.equal(undefined); - should(newProject.files.find(f => f.type === EntryType.File && f.relativePath === convertSlashesForSqlProj(scriptPath))).not.equal(undefined); - should(newProject.files.find(f => f.type === EntryType.File && f.relativePath === convertSlashesForSqlProj(scriptPathTagged))).not.equal(undefined); - should(newProject.files.find(f => f.type === EntryType.File && f.relativePath === convertSlashesForSqlProj(scriptPathTagged))?.sqlObjectType).equal(constants.ExternalStreamingJob); - - const newScriptContents = (await fs.readFile(path.join(newProject.projectFolderPath, scriptPath))).toString(); - - should(newScriptContents).equal(scriptContents); + should(project.folders.find(f => f.relativePath === convertSlashesForSqlProj(folderPath))).not.equal(undefined); + should(project.files.find(f => f.relativePath === convertSlashesForSqlProj(scriptPath))).not.equal(undefined); + should(project.files.find(f => f.relativePath === convertSlashesForSqlProj(scriptPathTagged))).not.equal(undefined); + // TODO: support for tagged entries not supported in DacFx.Projects + //should(project.files.find(f => f.relativePath === convertSlashesForSqlProj(scriptPathTagged))?.sqlObjectType).equal(constants.ExternalStreamingJob); }); - it('Should add Folder and Build entries to sqlproj with pre-existing scripts on disk', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const project = await Project.openProject(projFilePath); + it('Should bulk-add scripts to sqlproj with pre-existing scripts on disk', async function (): Promise { + const project = await testUtils.createTestSqlProject(this.test); - let list: Uri[] = await testUtils.createListOfFiles(path.dirname(projFilePath)); + // initial setup + (project.files.length).should.equal(0, 'initial number of scripts'); - await project.addToProject(list); + // create files on disk + const tablePath = path.join(project.projectFolderPath, 'MyTable.sql'); + await fs.writeFile(tablePath, 'CREATE TABLE [MyTable] ([Name] [nvarchar(50)'); - should(project.files.filter(f => f.type === EntryType.File).length).equal(11); // txt file shouldn't be added to the project - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(2); // 2 folders + const viewPath = path.join(project.projectFolderPath, 'MyView.sql'); + await fs.writeFile(viewPath, 'CREATE VIEW [MyView] AS SELECT * FROM [MyTable]'); + + // add to project + await project.addSqlObjectScripts(['MyTable.sql', 'MyView.sql']); + + // verify result + (project.files.length).should.equal(2, 'Number of scripts after adding'); }); - it('Should throw error while adding Folder and Build entries to sqlproj when a file/folder does not exist on disk', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const project = await Project.openProject(projFilePath); + // TODO: move to DacFx once script contents supported + it('Should throw error while adding folders and SQL object scripts to sqlproj when a file does not exist on disk', async function (): Promise { + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.openProjectFileBaseline); + const project = await testUtils.createTestSqlProject(this.test); let list: Uri[] = []; - let testFolderPath: string = await testUtils.createDummyFileStructure(true, list, path.dirname(projFilePath)); + let testFolderPath: string = await testUtils.createDummyFileStructure(this.test, true, list, path.dirname(projFilePath)); const nonexistentFile = path.join(testFolderPath, 'nonexistentFile.sql'); list.push(Uri.file(nonexistentFile)); - await testUtils.shouldThrowSpecificError(async () => await project.addToProject(list), constants.fileOrFolderDoesNotExist(Uri.file(nonexistentFile).fsPath)); + const relativePaths = list.map(f => path.relative(project.projectFolderPath, f.fsPath)); + + await testUtils.shouldThrowSpecificError(async () => await project.addSqlObjectScripts(relativePaths), `Error: No script found at '${nonexistentFile}'`); }); - it('Should choose correct master dacpac', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const project = await Project.openProject(projFilePath); + it('Should perform pre-deployment script operations', async function (): Promise { + let project = await testUtils.createTestSqlProject(this.test); - let uri = project.getSystemDacpacUri(constants.masterDacpac); - let ssdtUri = project.getSystemDacpacSsdtUri(constants.masterDacpac); - should.equal(uri.fsPath, Uri.parse(path.join('$(NETCoreTargetsPath)', 'SystemDacpacs', '160', constants.masterDacpac)).fsPath); - should.equal(ssdtUri.fsPath, Uri.parse(path.join('$(DacPacRootPath)', 'Extensions', 'Microsoft', 'SQLDB', 'Extensions', 'SqlServer', '160', 'SqlSchemas', constants.masterDacpac)).fsPath); + const relativePath = 'Script.PreDeployment1.sql'; + const absolutePath = path.join(project.projectFolderPath, relativePath); + const fileContents = 'SELECT 7'; - await project.changeTargetPlatform(constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlServer2016)!); - uri = project.getSystemDacpacUri(constants.masterDacpac); - ssdtUri = project.getSystemDacpacSsdtUri(constants.masterDacpac); - should.equal(uri.fsPath, Uri.parse(path.join('$(NETCoreTargetsPath)', 'SystemDacpacs', '130', constants.masterDacpac)).fsPath); - should.equal(ssdtUri.fsPath, Uri.parse(path.join('$(DacPacRootPath)', 'Extensions', 'Microsoft', 'SQLDB', 'Extensions', 'SqlServer', '130', 'SqlSchemas', constants.masterDacpac)).fsPath); + // initial state + (project.preDeployScripts.length).should.equal(0, 'initial state'); + (await exists(absolutePath)).should.be.false('inital state'); - await project.changeTargetPlatform(constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlAzure)!); - uri = project.getSystemDacpacUri(constants.masterDacpac); - ssdtUri = project.getSystemDacpacSsdtUri(constants.masterDacpac); - should.equal(uri.fsPath, Uri.parse(path.join('$(NETCoreTargetsPath)', 'SystemDacpacs', 'AzureV12', constants.masterDacpac)).fsPath); - should.equal(ssdtUri.fsPath, Uri.parse(path.join('$(DacPacRootPath)', 'Extensions', 'Microsoft', 'SQLDB', 'Extensions', 'SqlServer', 'AzureV12', 'SqlSchemas', constants.masterDacpac)).fsPath); + // add new + await project.addScriptItem(relativePath, fileContents, ItemType.preDeployScript); + (project.preDeployScripts.length).should.equal(1); + (await exists(absolutePath)).should.be.true('add new'); - await project.changeTargetPlatform(constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlDW)!); - uri = project.getSystemDacpacUri(constants.masterDacpac); - ssdtUri = project.getSystemDacpacSsdtUri(constants.masterDacpac); - should.equal(uri.fsPath, Uri.parse(path.join('$(NETCoreTargetsPath)', 'SystemDacpacs', 'AzureDw', constants.masterDacpac)).fsPath); - should.equal(ssdtUri.fsPath, Uri.parse(path.join('$(DacPacRootPath)', 'Extensions', 'Microsoft', 'SQLDB', 'Extensions', 'SqlServer', 'AzureDw', 'SqlSchemas', constants.masterDacpac)).fsPath); - }); + // read + project = await Project.openProject(project.projectFilePath); + (project.preDeployScripts.length).should.equal(1, 'read'); + (project.preDeployScripts[0].relativePath).should.equal(relativePath, 'read'); + // exclude + await project.excludePreDeploymentScript(relativePath); + (project.preDeployScripts.length).should.equal(0, 'exclude'); + (await exists(absolutePath)).should.be.true('exclude'); - it('Should update system dacpac paths in sqlproj when target platform is changed', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const project = await Project.openProject(projFilePath); - await project.addSystemDatabaseReference({ - systemDb: SystemDatabase.master, - suppressMissingDependenciesErrors: false - }); + // add existing + await project.addScriptItem(relativePath, undefined, ItemType.preDeployScript); + (project.preDeployScripts.length).should.equal(1, 'add existing'); - let projFileText = await fs.readFile(projFilePath); - - should.equal(project.databaseReferences.length, 1, 'System db reference should have been added'); - should(projFileText.includes(convertSlashesForSqlProj(Uri.file(path.join('$(NETCoreTargetsPath)', 'SystemDacpacs', '160', constants.masterDacpac)).fsPath.substring(1)))).be.true('System db reference path should be 160'); - should(projFileText.includes(convertSlashesForSqlProj(Uri.file(path.join('$(DacPacRootPath)', 'Extensions', 'Microsoft', 'SQLDB', 'Extensions', 'SqlServer', '160', 'SqlSchemas', constants.masterDacpac)).fsPath.substring(1)))).be.true('System db SSDT reference path should be 160'); - - await project.changeTargetPlatform(constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlServer2016)!); - projFileText = await fs.readFile(projFilePath); - should(projFileText.includes(convertSlashesForSqlProj(Uri.file(path.join('$(NETCoreTargetsPath)', 'SystemDacpacs', '130', constants.masterDacpac)).fsPath.substring(1)))).be.true('System db reference path should have been updated to 130'); - should(projFileText.includes(convertSlashesForSqlProj(Uri.file(path.join('$(DacPacRootPath)', 'Extensions', 'Microsoft', 'SQLDB', 'Extensions', 'SqlServer', '130', 'SqlSchemas', constants.masterDacpac)).fsPath.substring(1)))).be.true('System db SSDT reference path should be 130'); - - await project.changeTargetPlatform(constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlAzure)!); - projFileText = await fs.readFile(projFilePath); - should(projFileText.includes(convertSlashesForSqlProj(Uri.file(path.join('$(NETCoreTargetsPath)', 'SystemDacpacs', 'AzureV12', constants.masterDacpac)).fsPath.substring(1)))).be.true('System db reference path should have been updated to AzureV12'); - should(projFileText.includes(convertSlashesForSqlProj(Uri.file(path.join('$(DacPacRootPath)', 'Extensions', 'Microsoft', 'SQLDB', 'Extensions', 'SqlServer', 'AzureV12', 'SqlSchemas', constants.masterDacpac)).fsPath.substring(1)))).be.true('System db SSDT reference path should be AzureV12'); - - await project.changeTargetPlatform(constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlDW)!); - projFileText = await fs.readFile(projFilePath); - should(projFileText.includes(convertSlashesForSqlProj(Uri.file(path.join('$(NETCoreTargetsPath)', 'SystemDacpacs', 'AzureDw', constants.masterDacpac)).fsPath.substring(1)))).be.true('System db reference path should have been updated to AzureDw'); - should(projFileText.includes(convertSlashesForSqlProj(Uri.file(path.join('$(DacPacRootPath)', 'Extensions', 'Microsoft', 'SQLDB', 'Extensions', 'SqlServer', 'AzureDw', 'SqlSchemas', constants.masterDacpac)).fsPath.substring(1)))).be.true('System db SSDT reference path should be AzureDw'); - }); - - it('Should choose correct msdb dacpac', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const project = await Project.openProject(projFilePath); - - let uri = project.getSystemDacpacUri(constants.msdbDacpac); - let ssdtUri = project.getSystemDacpacSsdtUri(constants.msdbDacpac); - should.equal(uri.fsPath, Uri.parse(path.join('$(NETCoreTargetsPath)', 'SystemDacpacs', '160', constants.msdbDacpac)).fsPath); - should.equal(ssdtUri.fsPath, Uri.parse(path.join('$(DacPacRootPath)', 'Extensions', 'Microsoft', 'SQLDB', 'Extensions', 'SqlServer', '160', 'SqlSchemas', constants.msdbDacpac)).fsPath); - - await project.changeTargetPlatform(constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlServer2016)!); - uri = project.getSystemDacpacUri(constants.msdbDacpac); - ssdtUri = project.getSystemDacpacSsdtUri(constants.msdbDacpac); - should.equal(uri.fsPath, Uri.parse(path.join('$(NETCoreTargetsPath)', 'SystemDacpacs', '130', constants.msdbDacpac)).fsPath); - should.equal(ssdtUri.fsPath, Uri.parse(path.join('$(DacPacRootPath)', 'Extensions', 'Microsoft', 'SQLDB', 'Extensions', 'SqlServer', '130', 'SqlSchemas', constants.msdbDacpac)).fsPath); - }); - - it('Should throw error when choosing correct master dacpac if invalid DSP', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const project = await Project.openProject(projFilePath); - - await project.changeTargetPlatform('invalidPlatform'); - await testUtils.shouldThrowSpecificError(() => project.getSystemDacpacUri(constants.masterDacpac), constants.invalidDataSchemaProvider); - }); - - it('Should add system database references correctly', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const project = await Project.openProject(projFilePath); - - should(project.databaseReferences.length).equal(0, 'There should be no database references to start with'); - await project.addSystemDatabaseReference({ databaseName: 'master', systemDb: SystemDatabase.master, suppressMissingDependenciesErrors: false }); - should(project.databaseReferences.length).equal(1, 'There should be one database reference after adding a reference to master'); - should(project.databaseReferences[0].databaseName).equal(constants.master, 'The database reference should be master'); - should(project.databaseReferences[0].suppressMissingDependenciesErrors).equal(false, 'project.databaseReferences[0].suppressMissingDependenciesErrors should be false'); - // make sure reference to ADS master dacpac and SSDT master dacpac was added - let projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText).containEql(convertSlashesForSqlProj(project.getSystemDacpacUri(constants.master).fsPath.substring(1))); - should(projFileText).containEql(convertSlashesForSqlProj(project.getSystemDacpacSsdtUri(constants.master).fsPath.substring(1))); - - await project.addSystemDatabaseReference({ databaseName: 'msdb', systemDb: SystemDatabase.msdb, suppressMissingDependenciesErrors: false }); - should(project.databaseReferences.length).equal(2, 'There should be two database references after adding a reference to msdb'); - should(project.databaseReferences[1].databaseName).equal(constants.msdb, 'The database reference should be msdb'); - should(project.databaseReferences[1].suppressMissingDependenciesErrors).equal(false, 'project.databaseReferences[1].suppressMissingDependenciesErrors should be false'); - // make sure reference to ADS msdb dacpac and SSDT msdb dacpac was added - projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText).containEql(convertSlashesForSqlProj(project.getSystemDacpacUri(constants.msdb).fsPath.substring(1))); - should(projFileText).containEql(convertSlashesForSqlProj(project.getSystemDacpacSsdtUri(constants.msdb).fsPath.substring(1))); - }); - - it('Should add a dacpac reference to the same database correctly', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const project = await Project.openProject(projFilePath); - - // add database reference in the same database - should(project.databaseReferences.length).equal(0, 'There should be no database references to start with'); - await project.addDatabaseReference({ dacpacFileLocation: Uri.file('test1.dacpac'), suppressMissingDependenciesErrors: true }); - should(project.databaseReferences.length).equal(1, 'There should be a database reference after adding a reference to test1'); - should(project.databaseReferences[0].databaseName).equal('test1', 'The database reference should be test1'); - should(project.databaseReferences[0].suppressMissingDependenciesErrors).equal(true, 'project.databaseReferences[0].suppressMissingDependenciesErrors should be true'); - // make sure reference to test.dacpac was added - let projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText).containEql('test1.dacpac'); - }); - - it('Should add a dacpac reference to a different database in the same server correctly', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const project = await Project.openProject(projFilePath); - - // add database reference to a different database on the same server - should(project.databaseReferences.length).equal(0, 'There should be no database references to start with'); - await project.addDatabaseReference({ - dacpacFileLocation: Uri.file('test2.dacpac'), - databaseName: 'test2DbName', - databaseVariable: 'test2Db', - suppressMissingDependenciesErrors: false - }); - should(project.databaseReferences.length).equal(1, 'There should be a database reference after adding a reference to test2'); - should(project.databaseReferences[0].databaseName).equal('test2', 'The database reference should be test2'); - should(project.databaseReferences[0].suppressMissingDependenciesErrors).equal(false, 'project.databaseReferences[0].suppressMissingDependenciesErrors should be false'); - // make sure reference to test2.dacpac and SQLCMD variable was added - let projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText).containEql('test2.dacpac'); - should(projFileText).containEql('test2Db'); - should(projFileText).containEql(''); - should(projFileText).containEql('test2DbName'); - }); - - it('Should add a dacpac reference to a different database in a different server correctly', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const project = await Project.openProject(projFilePath); - - // add database reference to a different database on a different server - should(project.databaseReferences.length).equal(0, 'There should be no database references to start with'); - await project.addDatabaseReference({ - dacpacFileLocation: Uri.file('test3.dacpac'), - databaseName: 'test3DbName', - databaseVariable: 'test3Db', - serverName: 'otherServerName', - serverVariable: 'otherServer', - suppressMissingDependenciesErrors: false - }); - should(project.databaseReferences.length).equal(1, 'There should be a database reference after adding a reference to test3'); - should(project.databaseReferences[0].databaseName).equal('test3', 'The database reference should be test3'); - should(project.databaseReferences[0].suppressMissingDependenciesErrors).equal(false, 'project.databaseReferences[0].suppressMissingDependenciesErrors should be false'); - // make sure reference to test3.dacpac and SQLCMD variables were added - let projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText).containEql('test3.dacpac'); - should(projFileText).containEql('test3Db'); - should(projFileText).containEql(''); - should(projFileText).containEql('test3DbName'); - should(projFileText).containEql('otherServer'); - should(projFileText).containEql(''); - should(projFileText).containEql('otherServerName'); - }); - - it('Should add a project reference to the same database correctly', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const project = await Project.openProject(projFilePath); - - // add database reference to a different database on a different server - should(project.databaseReferences.length).equal(0, 'There should be no database references to start with'); - should(Object.keys(project.sqlCmdVariables).length).equal(0, `There should be no sqlcmd variables to start with. Actual: ${Object.keys(project.sqlCmdVariables).length}`); - await project.addProjectReference({ - projectName: 'project1', - projectGuid: '', - projectRelativePath: Uri.file(path.join('..', 'project1', 'project1.sqlproj')), - suppressMissingDependenciesErrors: false - }); - should(project.databaseReferences.length).equal(1, 'There should be a database reference after adding a reference to project1'); - should(project.databaseReferences[0].databaseName).equal('project1', 'The database reference should be project1'); - should(project.databaseReferences[0].suppressMissingDependenciesErrors).equal(false, 'project.databaseReferences[0].suppressMissingDependenciesErrors should be false'); - should(Object.keys(project.sqlCmdVariables).length).equal(0, `There should be no sqlcmd variables added. Actual: ${Object.keys(project.sqlCmdVariables).length}`); - - // make sure reference to project1 and SQLCMD variables were added - let projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText).containEql('project1'); - }); - - it('Should add a project reference to a different database in the same server correctly', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const project = await Project.openProject(projFilePath); - - // add database reference to a different database on a different server - should(project.databaseReferences.length).equal(0, 'There should be no database references to start with'); - should(Object.keys(project.sqlCmdVariables).length).equal(0, 'There should be no sqlcmd variables to start with'); - await project.addProjectReference({ - projectName: 'project1', - projectGuid: '', - projectRelativePath: Uri.file(path.join('..', 'project1', 'project1.sqlproj')), - databaseName: 'testdbName', - databaseVariable: 'testdb', - suppressMissingDependenciesErrors: false - }); - should(project.databaseReferences.length).equal(1, 'There should be a database reference after adding a reference to project1'); - should(project.databaseReferences[0].databaseName).equal('project1', 'The database reference should be project1'); - should(project.databaseReferences[0].suppressMissingDependenciesErrors).equal(false, 'project.databaseReferences[0].suppressMissingDependenciesErrors should be false'); - should(Object.keys(project.sqlCmdVariables).length).equal(1, `There should be one new sqlcmd variable added. Actual: ${Object.keys(project.sqlCmdVariables).length}`); - - // make sure reference to project1 and SQLCMD variables were added - let projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText).containEql('project1'); - should(projFileText).containEql('testdb'); - should(projFileText).containEql(''); - should(projFileText).containEql('testdbName'); - }); - - it('Should add a project reference to a different database in a different server correctly', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const project = await Project.openProject(projFilePath); - - // add database reference to a different database on a different server - should(project.databaseReferences.length).equal(0, 'There should be no database references to start with'); - should(Object.keys(project.sqlCmdVariables).length).equal(0, 'There should be no sqlcmd variables to start with'); - await project.addProjectReference({ - projectName: 'project1', - projectGuid: '', - projectRelativePath: Uri.file(path.join('..', 'project1', 'project1.sqlproj')), - databaseName: 'testdbName', - databaseVariable: 'testdb', - serverName: 'otherServerName', - serverVariable: 'otherServer', - suppressMissingDependenciesErrors: false - }); - should(project.databaseReferences.length).equal(1, 'There should be a database reference after adding a reference to project1'); - should(project.databaseReferences[0].databaseName).equal('project1', 'The database reference should be project1'); - should(project.databaseReferences[0].suppressMissingDependenciesErrors).equal(false, 'project.databaseReferences[0].suppressMissingDependenciesErrors should be false'); - should(Object.keys(project.sqlCmdVariables).length).equal(2, `There should be two new sqlcmd variables added. Actual: ${Object.keys(project.sqlCmdVariables).length}`); - - // make sure reference to project1 and SQLCMD variables were added - let projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText).containEql('project1'); - should(projFileText).containEql('testdb'); - should(projFileText).containEql(''); - should(projFileText).containEql('testdbName'); - should(projFileText).containEql('otherServer'); - should(projFileText).containEql(''); - should(projFileText).containEql('otherServerName'); - }); - - it('Should not allow adding duplicate dacpac references', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const project = await Project.openProject(projFilePath); - - should(project.databaseReferences.length).equal(0, 'There should be no database references to start with'); - - const dacpacReference: IDacpacReferenceSettings = { dacpacFileLocation: Uri.file('test.dacpac'), suppressMissingDependenciesErrors: false }; - await project.addDatabaseReference(dacpacReference); - should(project.databaseReferences.length).equal(1, 'There should be one database reference after adding a reference to test.dacpac'); - should(project.databaseReferences[0].databaseName).equal('test', 'project.databaseReferences[0].databaseName should be test'); - - // try to add reference to test.dacpac again - await testUtils.shouldThrowSpecificError(async () => await project.addDatabaseReference(dacpacReference), constants.databaseReferenceAlreadyExists); - should(project.databaseReferences.length).equal(1, 'There should be one database reference after trying to add a reference to test.dacpac again'); - }); - - it('Should not allow adding duplicate system database references', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const project = await Project.openProject(projFilePath); - - should(project.databaseReferences.length).equal(0, 'There should be no database references to start with'); - - const systemDbReference: ISystemDatabaseReferenceSettings = { databaseName: 'master', systemDb: SystemDatabase.master, suppressMissingDependenciesErrors: false }; - await project.addSystemDatabaseReference(systemDbReference); - should(project.databaseReferences.length).equal(1, 'There should be one database reference after adding a reference to master'); - should(project.databaseReferences[0].databaseName).equal(constants.master, 'project.databaseReferences[0].databaseName should be master'); - - // try to add reference to master again - await testUtils.shouldThrowSpecificError(async () => await project.addSystemDatabaseReference(systemDbReference), constants.databaseReferenceAlreadyExists); - should(project.databaseReferences.length).equal(1, 'There should only be one database reference after trying to add a reference to master again'); - }); - - it('Should not allow adding duplicate project references', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const project = await Project.openProject(projFilePath); - - should(project.databaseReferences.length).equal(0, 'There should be no database references to start with'); - - const projectReference: IProjectReferenceSettings = { - projectName: 'testProject', - projectGuid: '', - projectRelativePath: Uri.file('testProject.sqlproj'), - suppressMissingDependenciesErrors: false - }; - await project.addProjectReference(projectReference); - should(project.databaseReferences.length).equal(1, 'There should be one database reference after adding a reference to testProject.sqlproj'); - should(project.databaseReferences[0].databaseName).equal('testProject', 'project.databaseReferences[0].databaseName should be testProject'); - - // try to add reference to testProject again - await testUtils.shouldThrowSpecificError(async () => await project.addProjectReference(projectReference), constants.databaseReferenceAlreadyExists); - should(project.databaseReferences.length).equal(1, 'There should be one database reference after trying to add a reference to testProject again'); - }); - - it('Should handle trying to add duplicate database references when slashes are different direction', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const project = await Project.openProject(projFilePath); - - should(project.databaseReferences.length).equal(0, 'There should be no database references to start with'); - - const projectReference: IProjectReferenceSettings = { - projectName: 'testProject', - projectGuid: '', - projectRelativePath: Uri.file('testFolder/testProject.sqlproj'), - suppressMissingDependenciesErrors: false - }; - await project.addProjectReference(projectReference); - should(project.databaseReferences.length).equal(1, 'There should be one database reference after adding a reference to testProject.sqlproj'); - should(project.databaseReferences[0].databaseName).equal('testProject', 'project.databaseReferences[0].databaseName should be testProject'); - - // try to add reference to testProject again with slashes in the other direction - projectReference.projectRelativePath = Uri.file('testFolder\\testProject.sqlproj'); - await testUtils.shouldThrowSpecificError(async () => await project.addProjectReference(projectReference), constants.databaseReferenceAlreadyExists); - should(project.databaseReferences.length).equal(1, 'There should be one database reference after trying to add a reference to testProject again'); - }); - - it('Should update sqlcmd variable values if value changes', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const project = await Project.openProject(projFilePath); - const databaseVariable = 'test3Db'; - const serverVariable = 'otherServer'; - - should(project.databaseReferences.length).equal(0, 'There should be no database references to start with'); - await project.addDatabaseReference({ - dacpacFileLocation: Uri.file('test3.dacpac'), - databaseName: 'test3DbName', - databaseVariable: databaseVariable, - serverName: 'otherServerName', - serverVariable: serverVariable, - suppressMissingDependenciesErrors: false - }); - 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(Object.keys(project.sqlCmdVariables).length).equal(2, 'There should be 2 sqlcmdvars after adding the dacpac reference'); - - // make sure reference to test3.dacpac and SQLCMD variables were added - let projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText).containEql(''); - should(projFileText).containEql('test3DbName'); - should(projFileText).containEql(''); - should(projFileText).containEql('otherServerName'); - - // delete reference - await project.deleteDatabaseReference(project.databaseReferences[0]); - should(project.databaseReferences.length).equal(0, 'There should be no database references after deleting'); - should(Object.keys(project.sqlCmdVariables).length).equal(2, 'There should still be 2 sqlcmdvars after deleting the dacpac reference'); - - // add reference to the same dacpac again but with different values for the sqlcmd variables - await project.addDatabaseReference({ - dacpacFileLocation: Uri.file('test3.dacpac'), - databaseName: 'newDbName', - databaseVariable: databaseVariable, - serverName: 'newServerName', - serverVariable: serverVariable, - suppressMissingDependenciesErrors: false - }); - 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(Object.keys(project.sqlCmdVariables).length).equal(2, 'There should still be 2 sqlcmdvars after adding the dacpac reference again with different sqlcmdvar values'); - - projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText).containEql(''); - should(projFileText).containEql('newDbName'); - should(projFileText).containEql(''); - should(projFileText).containEql('newServerName'); - }); - - it('Should add pre and post deployment scripts as entries to sqlproj', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const project: Project = await Project.openProject(projFilePath); - - const folderPath = 'Pre-Post Deployment Scripts'; - const preDeploymentScriptFilePath = path.join(folderPath, 'Script.PreDeployment1.sql'); - const postDeploymentScriptFilePath = path.join(folderPath, 'Script.PostDeployment1.sql'); - const fileContents = ' '; - - await project.addFolderItem(folderPath); - await project.addScriptItem(preDeploymentScriptFilePath, fileContents, ItemType.preDeployScript); - await project.addScriptItem(postDeploymentScriptFilePath, fileContents, ItemType.postDeployScript); - - const newProject = await Project.openProject(projFilePath); - - should(newProject.preDeployScripts.find(f => f.type === EntryType.File && f.relativePath === convertSlashesForSqlProj(preDeploymentScriptFilePath))).not.equal(undefined, 'File Script.PreDeployment1.sql not read'); - should(newProject.postDeployScripts.find(f => f.type === EntryType.File && f.relativePath === convertSlashesForSqlProj(postDeploymentScriptFilePath))).not.equal(undefined, 'File Script.PostDeployment1.sql not read'); + //delete + await project.deletePreDeploymentScript(relativePath); + (project.preDeployScripts.length).should.equal(0, 'delete'); + (await exists(absolutePath)).should.be.false('delete'); }); it('Should show information messages when adding more than one pre/post deployment scripts to sqlproj', async function (): Promise { const stub = sinon.stub(window, 'showInformationMessage').returns(Promise.resolve()); - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const project: Project = await Project.openProject(projFilePath); + const project: Project = await testUtils.createTestSqlProject(this.test); - const folderPath = 'Pre-Post Deployment Scripts'; - const preDeploymentScriptFilePath = path.join(folderPath, 'Script.PreDeployment1.sql'); - const postDeploymentScriptFilePath = path.join(folderPath, 'Script.PostDeployment1.sql'); - const preDeploymentScriptFilePath2 = path.join(folderPath, 'Script.PreDeployment2.sql'); - const postDeploymentScriptFilePath2 = path.join(folderPath, 'Script.PostDeployment2.sql'); - const fileContents = ' '; + const preDeploymentScriptFilePath = 'Script.PreDeployment1.sql'; + const postDeploymentScriptFilePath = 'Script.PostDeployment1.sql'; + const preDeploymentScriptFilePath2 = 'Script.PreDeployment2.sql'; + const postDeploymentScriptFilePath2 = 'Script.PostDeployment2.sql'; + const fileContents = 'SELECT 7'; - await project.addFolderItem(folderPath); await project.addScriptItem(preDeploymentScriptFilePath, fileContents, ItemType.preDeployScript); await project.addScriptItem(postDeploymentScriptFilePath, fileContents, ItemType.postDeployScript); + (stub.notCalled).should.be.true('showInformationMessage should not have been called'); + await project.addScriptItem(preDeploymentScriptFilePath2, fileContents, ItemType.preDeployScript); - should(stub.calledWith(constants.deployScriptExists(constants.PreDeploy))).be.true(`showInformationMessage not called with expected message '${constants.deployScriptExists(constants.PreDeploy)}' Actual '${stub.getCall(0).args[0]}'`); + (stub.calledOnce).should.be.true('showInformationMessage should have been called once after adding extra pre-deployment script'); + (stub.calledWith(constants.deployScriptExists(constants.PreDeploy))).should.be.true(`showInformationMessage not called with expected message '${constants.deployScriptExists(constants.PreDeploy)}'; actual: '${stub.firstCall.args[0]}'`); + + stub.resetHistory(); await project.addScriptItem(postDeploymentScriptFilePath2, fileContents, ItemType.postDeployScript); + (stub.calledOnce).should.be.true('showInformationMessage should have been called once after adding extra post-deployment script'); should(stub.calledWith(constants.deployScriptExists(constants.PostDeploy))).be.true(`showInformationMessage not called with expected message '${constants.deployScriptExists(constants.PostDeploy)}' Actual '${stub.getCall(0).args[0]}'`); - - const newProject = await Project.openProject(projFilePath); - - should(newProject.preDeployScripts.find(f => f.type === EntryType.File && f.relativePath === convertSlashesForSqlProj(preDeploymentScriptFilePath))).not.equal(undefined, 'File Script.PreDeployment1.sql not read'); - should(newProject.postDeployScripts.find(f => f.type === EntryType.File && f.relativePath === convertSlashesForSqlProj(postDeploymentScriptFilePath))).not.equal(undefined, 'File Script.PostDeployment1.sql not read'); - should(newProject.noneDeployScripts.length).equal(2); - should(newProject.noneDeployScripts.find(f => f.type === EntryType.File && f.relativePath === convertSlashesForSqlProj(preDeploymentScriptFilePath2))).not.equal(undefined, 'File Script.PreDeployment2.sql not read'); - should(newProject.noneDeployScripts.find(f => f.type === EntryType.File && f.relativePath === convertSlashesForSqlProj(postDeploymentScriptFilePath2))).not.equal(undefined, 'File Script.PostDeployment2.sql not read'); - - }); - - it('Should ignore duplicate file/folder entries in new sqlproj', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const project: Project = await Project.openProject(projFilePath); - const fileList = await testUtils.createListOfFiles(path.dirname(projFilePath)); - - // 1. Add a folder to the project - const existingFolderUri = fileList[2]; - const folderStats = await fs.stat(existingFolderUri.fsPath); - should(folderStats.isDirectory()).equal(true, 'Third entry in fileList should be a subfolder'); - - const folderEntry = await project.addToProject([existingFolderUri]); - should(project.files.length).equal(1, 'New folder entry should be added to the project'); - - // Add the folder to the project again - should(await project.addToProject([existingFolderUri])) - .equal(folderEntry, 'Original folder entry should be returned when adding same folder for a second time'); - should(project.files.length).equal(1, 'No new entries should be added to the project when adding same folder for a second time'); - - // 2. Add a file to the project - let existingFileUri = fileList[1]; - let fileStats = await fs.stat(existingFileUri.fsPath); - should(fileStats.isFile()).equal(true, 'Second entry in fileList should be a file'); - - let fileEntry = await project.addToProject([existingFileUri]); - should(project.files.length).equal(2, 'New file entry should be added to the project'); - - // Add the file to the project again - should(await project.addToProject([existingFileUri])) - .equal(fileEntry, 'Original file entry should be returned when adding same file for a second time'); - should(project.files.length).equal(2, 'No new entries should be added to the project when adding same file for a second time'); - - // 3. Add a file from subfolder to the project - existingFileUri = fileList[3]; - fileStats = await fs.stat(existingFileUri.fsPath); - should(fileStats.isFile()).equal(true, 'Fourth entry in fileList should be a file'); - - fileEntry = await project.addToProject([existingFileUri]); - should(project.files.length).equal(3, 'New file entry should be added to the project'); - - // Add the file from subfolder to the project again - should(await project.addToProject([existingFileUri])) - .equal(fileEntry, 'Original file entry should be returned when adding same file for a second time'); - should(project.files.length).equal(3, 'No new entries should be added to the project when adding same file for a second time'); - }); - - it('Should ignore duplicate file entries in existing sqlproj', async function (): Promise { - // Create new sqlproj - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const fileList = await testUtils.createListOfFiles(path.dirname(projFilePath)); - - let project: Project = await Project.openProject(projFilePath); - - // Add a file to the project - let existingFileUri = fileList[3]; - let fileStats = await fs.stat(existingFileUri.fsPath); - should(fileStats.isFile()).equal(true, 'Fourth entry in fileList should be a file'); - await project.addToProject([existingFileUri]); - - // Reopen existing project - project = await Project.openProject(projFilePath); - - // Try adding the same file to the project again - await project.addToProject([existingFileUri]); }); + // TODO: move to DacFx once script contents supported it('Should not overwrite existing files', async function (): Promise { // Create new sqlproj - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const fileList = await testUtils.createListOfFiles(path.dirname(projFilePath)); + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.newProjectFileBaseline); + const fileList = await testUtils.createListOfFiles(this.test, path.dirname(projFilePath)); let project: Project = await Project.openProject(projFilePath); @@ -676,242 +252,61 @@ describe('Project: sqlproj content operations', function (): void { `A file with the name '${path.parse(relativePath).name}' already exists on disk at this location. Please choose another name.`); }); - it('Should not add folders outside of the project folder', async function (): Promise { + // TODO: revisit correct behavior for this, since DacFx.Projects makes no restriction on absolute paths and external folders (which are represented as "..") + it.skip('Should not add folders outside of the project folder', async function (): Promise { // Create new sqlproj - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.newProjectFileBaseline); let project: Project = await Project.openProject(projFilePath); // Try adding project root folder itself - this is silently ignored - await project.addToProject([Uri.file(path.dirname(projFilePath))]); + await project.addFolder(path.dirname(projFilePath)); should.equal(project.files.length, 0, 'Nothing should be added to the project'); // Try adding a parent of the project folder await testUtils.shouldThrowSpecificError( - async () => await project.addToProject([Uri.file(path.dirname(path.dirname(projFilePath)))]), + async () => await project.addFolder(path.dirname(path.dirname(projFilePath))), 'Items with absolute path outside project folder are not supported. Please make sure the paths in the project file are relative to project folder.', 'Folders outside the project folder should not be added.'); }); - it('Project entry relative path should not change after reload', async function (): Promise { - // Create new sqlproj - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const projectFolder = path.dirname(projFilePath); - - // Create file under nested folders structure - const newFile = path.join(projectFolder, 'foo', 'test.sql'); - await fs.mkdir(path.dirname(newFile), { recursive: true }); - await fs.writeFile(newFile, ''); - - let project: Project = await Project.openProject(projFilePath); - - // Add a file to the project - await project.addToProject([Uri.file(newFile)]); - - // Store the original `relativePath` of the project entry - let fileEntry = project.files.find(f => f.relativePath.endsWith('test.sql')); - - should.exist(fileEntry, 'Entry for the file should be added to project'); - let originalRelativePath = ''; - if (fileEntry) { - originalRelativePath = fileEntry.relativePath; - } - - // Reopen existing project - project = await Project.openProject(projFilePath); - - // Validate that relative path of the file entry matches the original - // There will be additional folder - should(project.files.length).equal(2, 'Two entries are expected in the loaded project'); - - fileEntry = project.files.find(f => f.relativePath.endsWith('test.sql')); - should.exist(fileEntry, 'Entry for the file should be present in the project after reload'); - if (fileEntry) { - should(fileEntry.relativePath).equal(originalRelativePath, 'Relative path should match after reload'); - } - }); - - it('Intermediate folders for file should be automatically added to project', async function (): Promise { - // Create new sqlproj - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const projectFolder = path.dirname(projFilePath); - - // Create file under nested folders structure - const newFile = path.join(projectFolder, 'foo', 'bar', 'test.sql'); - await fs.mkdir(path.dirname(newFile), { recursive: true }); - await fs.writeFile(newFile, ''); - - // Open empty project - let project: Project = await Project.openProject(projFilePath); - - // Add a file to the project - await project.addToProject([Uri.file(newFile)]); - - // Validate that intermediate folders were added to the project - should(project.files.length).equal(3, 'Three entries are expected in the project'); - should(project.files.map(f => ({ type: f.type, relativePath: f.relativePath }))) - .containDeep([ - { type: EntryType.Folder, relativePath: 'foo\\' }, - { type: EntryType.Folder, relativePath: 'foo\\bar\\' }, - { type: EntryType.File, relativePath: 'foo\\bar\\test.sql' }]); - }); - - it('Intermediate folders for folder should be automatically added to project', async function (): Promise { - // Create new sqlproj - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const projectFolder = path.dirname(projFilePath); - - // Create nested folders structure - const newFolder = path.join(projectFolder, 'foo', 'bar'); - await fs.mkdir(newFolder, { recursive: true }); - - // Open empty project - let project: Project = await Project.openProject(projFilePath); - - // Add a file to the project - await project.addToProject([Uri.file(newFolder)]); - - // Validate that intermediate folders were added to the project - should(project.files.length).equal(2, 'Two entries are expected in the project'); - should(project.files.map(f => ({ type: f.type, relativePath: f.relativePath }))) - .containDeep([ - { type: EntryType.Folder, relativePath: 'foo\\' }, - { type: EntryType.Folder, relativePath: 'foo\\bar\\' }]); - }); - - it('Should not add duplicate intermediate folders to project', async function (): Promise { - // Create new sqlproj - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const projectFolder = path.dirname(projFilePath); - - // Create file under nested folders structure - const newFile = path.join(projectFolder, 'foo', 'bar', 'test.sql'); - await fs.mkdir(path.dirname(newFile), { recursive: true }); - await fs.writeFile(newFile, ''); - - const anotherNewFile = path.join(projectFolder, 'foo', 'bar', 'test2.sql'); - await fs.writeFile(anotherNewFile, ''); - - // Open empty project - let project: Project = await Project.openProject(projFilePath); - - // Add a file to the project - await project.addToProject([Uri.file(newFile)]); - await project.addToProject([Uri.file(anotherNewFile)]); - - // Validate that intermediate folders were added to the project - should(project.files.length).equal(4, 'Four entries are expected in the project'); - should(project.files.map(f => ({ type: f.type, relativePath: f.relativePath }))) - .containDeep([ - { type: EntryType.Folder, relativePath: 'foo\\' }, - { type: EntryType.Folder, relativePath: 'foo\\bar\\' }, - { type: EntryType.File, relativePath: 'foo\\bar\\test.sql' }, - { type: EntryType.File, relativePath: 'foo\\bar\\test2.sql' }]); - }); - - it('Should not add duplicate intermediate folders to project when folder is explicitly added', async function (): Promise { - // Create new sqlproj - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const projectFolder = path.dirname(projFilePath); - - // Create file under nested folders structure - const newFile = path.join(projectFolder, 'foo', 'bar', 'test.sql'); - await fs.mkdir(path.dirname(newFile), { recursive: true }); - await fs.writeFile(newFile, ''); - - const explicitIntermediateFolder = path.join(projectFolder, 'foo', 'bar'); - await fs.mkdir(explicitIntermediateFolder, { recursive: true }); - - // Open empty project - let project: Project = await Project.openProject(projFilePath); - - // Add file and folder to the project - await project.addToProject([Uri.file(newFile), Uri.file(explicitIntermediateFolder)]); - - // Validate that intermediate folders were added to the project - should(project.files.length).equal(3, 'Three entries are expected in the project'); - should(project.files.map(f => ({ type: f.type, relativePath: f.relativePath }))) - .containDeep([ - { type: EntryType.Folder, relativePath: 'foo\\' }, - { type: EntryType.Folder, relativePath: 'foo\\bar\\' }, - { type: EntryType.File, relativePath: 'foo\\bar\\test.sql' }]); - }); - it('Should handle adding existing items to project', async function (): Promise { // Create new sqlproj - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const projectFolder = path.dirname(projFilePath); - + const project: Project = await testUtils.createTestSqlProject(this.test); // Create 2 new files, a sql file and a txt file - const sqlFile = path.join(projectFolder, 'test.sql'); - const txtFile = path.join(projectFolder, 'foo', 'test.txt'); - await fs.writeFile(sqlFile, ''); + const sqlFile = path.join(project.projectFolderPath, 'test.sql'); + const txtFile = path.join(project.projectFolderPath, 'foo', 'test.txt'); + await fs.writeFile(sqlFile, 'CREATE TABLE T1 (C1 INT)'); await fs.mkdir(path.dirname(txtFile)); - await fs.writeFile(txtFile, ''); + await fs.writeFile(txtFile, 'Hello World!'); - const project: Project = await Project.openProject(projFilePath); + await project.readProjFile(); // Add them as existing files + await project.addFolder('foo'); // TODO: This shouldn't be necessary; DacFx.Projects needs to refresh the in-memory folder list internally after adding items await project.addExistingItem(sqlFile); await project.addExistingItem(txtFile); // Validate files should have been added to project - should(project.files.length).equal(3, 'Three entries are expected in the project'); - should(project.files.map(f => ({ type: f.type, relativePath: f.relativePath }))) - .containDeep([ - { type: EntryType.Folder, relativePath: 'foo\\' }, - { type: EntryType.File, relativePath: 'test.sql' }, - { type: EntryType.File, relativePath: 'foo\\test.txt' }]); + (project.files.length).should.equal(1, `SQL script object count: ${project.files.map(x => x.relativePath).join('; ')}`); + (project.files[0].relativePath).should.equal('test.sql'); - // Validate project file XML - const projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText.includes('')).equal(true, projFileText); - should(projFileText.includes('')).equal(true, projFileText); + should(project.folders.length).equal(1, 'folders'); + (project.folders[0].relativePath).should.equal('foo'); + + should(project.noneDeployScripts.length).equal(1, ' items'); + (project.noneDeployScripts[0].relativePath).should.equal('foo\\test.txt'); }); - it('Should read OutputPath from sqlproj if there is one for legacy-style project with Debug configuration', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline); + it('Should read project properties', async function (): Promise { + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.sqlProjPropertyReadBaseline); const project: Project = await Project.openProject(projFilePath); - should(project.configuration).equal('Debug'); - should(project.outputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath('bin\\Debug\\'))); - should(project.dacpacOutputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath('bin\\Debug\\'), `${project.projectFileName}.dacpac`)); - }); - - it('Should read OutputPath from sqlproj if there is one for legacy-style project with Release configuration', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.openProjectFileReleaseConfigurationBaseline); - const project: Project = await Project.openProject(projFilePath); - - should(project.configuration).equal('Release'); - should(project.outputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath('bin\\Release\\'))); - should(project.dacpacOutputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath('bin\\Release\\'), `${project.projectFileName}.dacpac`)); - }); - - it('Should set configuration to Output for legacy-style project with unknown configuration', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.openProjectFileUnknownConfigurationBaseline); - const project: Project = await Project.openProject(projFilePath); - - should(project.configuration).equal('Output'); - should(project.outputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath('bin\\Output'))); - should(project.dacpacOutputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath('bin\\Output\\'), `${project.projectFileName}.dacpac`)); - }); - - it('Should set configuration to Output for legacy-style project with unknown configuration', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.openProjectFileSingleOutputPathBaseline); - const project: Project = await Project.openProject(projFilePath); - - should(project.configuration).equal('Debug'); - should(project.outputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath('..\\otherFolder'))); - should(project.dacpacOutputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath('..\\otherFolder'), `${project.projectFileName}.dacpac`)); - }); - - it('Should use the last OutputPath in the .sqlproj that matches the conditions', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.openProjectFileMultipleOutputPathBaseline); - const project: Project = await Project.openProject(projFilePath); - - should(project.configuration).equal('Debug'); - should(project.outputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath('bin\\other'))); - should(project.dacpacOutputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath('bin\\other'), `${project.projectFileName}.dacpac`)); + (project.sqlProjStyle).should.equal(ProjectType.SdkStyle); + (project.outputPath).should.equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath('CustomOutputPath\\Dacpacs\\'))); + (project.configuration).should.equal('Release'); + (project.getDatabaseSourceValues()).should.deepEqual(['oneSource', 'twoSource', 'redSource', 'blueSource']); + (project.getProjectTargetVersion()).should.equal('130'); }); }); @@ -928,160 +323,9 @@ describe('Project: sdk style project content operations', function (): void { await testUtils.deleteGeneratedTestFolder(); }); - it('Should read project from sqlproj and files and folders by globbing', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectBaseline); - await testUtils.createDummyFileStructureWithPrePostDeployScripts(false, undefined, path.dirname(projFilePath)); - const project: Project = await Project.openProject(projFilePath); - - // Files and folders - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(3); - should(project.files.filter(f => f.type === EntryType.File).length).equal(13); - - // SqlCmdVariables - should(Object.keys(project.sqlCmdVariables).length).equal(2); - should(project.sqlCmdVariables['ProdDatabaseName']).equal('MyProdDatabase'); - should(project.sqlCmdVariables['BackupDatabaseName']).equal('MyBackupDatabase'); - - // Database references - // should only have one database reference even though there are two master.dacpac references (1 for ADS and 1 for SSDT) - should(project.databaseReferences.length).equal(1); - should(project.databaseReferences[0].databaseName).containEql(constants.master); - should(project.databaseReferences[0] instanceof SystemDatabaseReferenceProjectEntry).equal(true); - - // // Pre-post deployment scripts - should(project.preDeployScripts.length).equal(1); - should(project.postDeployScripts.length).equal(1); - should(project.noneDeployScripts.length).equal(2); - should(project.preDeployScripts.find(f => f.type === EntryType.File && f.relativePath === 'Script.PreDeployment1.sql')).not.equal(undefined, 'File Script.PreDeployment1.sql not read'); - should(project.noneDeployScripts.find(f => f.type === EntryType.File && f.relativePath === 'Script.PreDeployment2.sql')).not.equal(undefined, 'File Script.PreDeployment2.sql not read'); - should(project.postDeployScripts.find(f => f.type === EntryType.File && f.relativePath === 'Script.PostDeployment1.sql')).not.equal(undefined, 'File Script.PostDeployment1.sql not read'); - should(project.noneDeployScripts.find(f => f.type === EntryType.File && f.relativePath === 'folder1\\Script.PostDeployment2.sql')).not.equal(undefined, 'File folder1\\Script.PostDeployment2.sql not read'); - }); - - it('Should handle files listed in sqlproj', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectWithFilesSpecifiedBaseline); - await testUtils.createDummyFileStructure(false, undefined, path.dirname(projFilePath)); - - const project: Project = await Project.openProject(projFilePath); - - // Files and folders - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(2); - should(project.files.filter(f => f.type === EntryType.File).length).equal(11); - - // these are also listed in the sqlproj, but there shouldn't be duplicate entries for them - should(project.files.filter(f => f.relativePath === 'folder1\\file2.sql').length).equal(1); - should(project.files.filter(f => f.relativePath === 'file1.sql').length).equal(1); - should(project.files.filter(f => f.relativePath === 'folder1\\').length).equal(1); - }); - - it('Should handle pre/post/none deploy scripts outside of project folder', async function (): Promise { - const testFolderPath = await testUtils.generateTestFolderPath(); - const mainProjectPath = path.join(testFolderPath, 'project'); - const otherFolderPath = path.join(testFolderPath, 'other'); - projFilePath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectWithGlobsSpecifiedBaseline, mainProjectPath); - await testUtils.createDummyFileStructure(false, undefined, path.dirname(projFilePath)); - - // create files outside of project folder that are included in the project file - await fs.mkdir(otherFolderPath); - await testUtils.createOtherDummyFiles(otherFolderPath); - - const project: Project = await Project.openProject(projFilePath); - - // verify files, folders, pre/post/none deploy scripts were loaded correctly - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(2); - should(project.files.filter(f => f.type === EntryType.File).length).equal(18); - should(project.preDeployScripts.length).equal(1); - should(project.postDeployScripts.length).equal(1); - should(project.noneDeployScripts.length).equal(1); - }); - - it('Should handle globbing patterns listed in sqlproj', async function (): Promise { - const testFolderPath = await testUtils.generateTestFolderPath(); - const mainProjectPath = path.join(testFolderPath, 'project'); - const otherFolderPath = path.join(testFolderPath, 'other'); - projFilePath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectWithGlobsSpecifiedBaseline, mainProjectPath); - await testUtils.createDummyFileStructure(false, undefined, path.dirname(projFilePath)); - - // create files outside of project folder that are included in the project file - await fs.mkdir(otherFolderPath); - await testUtils.createOtherDummyFiles(otherFolderPath); - - const project: Project = await Project.openProject(projFilePath); - - should(project.files.filter(f => f.type === EntryType.File).length).equal(18); - - // make sure all the correct files from the globbing patterns were included - // ..\other\folder1\test?.sql - should(project.files.filter(f => f.relativePath === '..\\other\\folder1\\test1.sql').length).equal(1); - should(project.files.filter(f => f.relativePath === '..\\other\\folder1\\test2.sql').length).equal(1); - should(project.files.filter(f => f.relativePath === '..\\other\\folder1\\testLongerName.sql').length).equal(0); - - // ..\other\folder1\file*.sql - should(project.files.filter(f => f.relativePath === '..\\other\\folder1\\file1.sql').length).equal(1); - should(project.files.filter(f => f.relativePath === '..\\other\\folder1\\file2.sql').length).equal(1); - - // ..\other\folder2\*.sql - should(project.files.filter(f => f.relativePath === '..\\other\\folder2\\file1.sql').length).equal(1); - should(project.files.filter(f => f.relativePath === '..\\other\\folder2\\file2.sql').length).equal(1); - - // make sure no duplicates from folder1\*.sql - should(project.files.filter(f => f.relativePath === 'folder1\\file1.sql').length).equal(1); - }); - - it('Should handle Build Remove in sqlproj', async function (): Promise { - const testFolderPath = await testUtils.generateTestFolderPath(); - const mainProjectPath = path.join(testFolderPath, 'project'); - const otherFolderPath = path.join(testFolderPath, 'other'); - projFilePath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectWithBuildRemoveBaseline, mainProjectPath); - await testUtils.createDummyFileStructure(false, undefined, path.dirname(projFilePath)); - - // create files outside of project folder that are included in the project file - await fs.mkdir(otherFolderPath); - await testUtils.createOtherDummyFiles(otherFolderPath); - - const project: Project = await Project.openProject(projFilePath); - - should(project.files.filter(f => f.type === EntryType.File).length).equal(7); - - // make sure all the correct files from the globbing patterns were included and removed are evaluated in order - - // - // - // expected: ..\other\folder1\file1.sql is not included - should(project.files.filter(f => f.relativePath === '..\\other\\folder1\\file1.sql').length).equal(0); - should(project.files.filter(f => f.relativePath === '..\\other\\folder1\\file2.sql').length).equal(1); - - // - // - // expected: ..\other\folder2\file2.sql is not included - should(project.files.filter(f => f.relativePath === '..\\other\\folder2\\file1.sql').length).equal(0); - should(project.files.filter(f => f.relativePath === '..\\other\\folder2\\file2.sql').length).equal(0); - - // - // - // expected: folder1\file2.sql is included - should(project.files.filter(f => f.relativePath === 'folder1\\file1.sql').length).equal(0); - should(project.files.filter(f => f.relativePath === 'folder1\\file2.sql').length).equal(1); - should(project.files.filter(f => f.relativePath === 'folder1\\file3.sql').length).equal(0); - should(project.files.filter(f => f.relativePath === 'folder1\\file4.sql').length).equal(0); - should(project.files.filter(f => f.relativePath === 'folder1\\file5.sql').length).equal(0); - - // - // - // expected: folder2\file3.sql is included - should(project.files.filter(f => f.relativePath === 'folder2\\file1.sql').length).equal(1); - should(project.files.filter(f => f.relativePath === 'folder2\\file2.sql').length).equal(1); - should(project.files.filter(f => f.relativePath === 'folder2\\file3.sql').length).equal(1); - should(project.files.filter(f => f.relativePath === 'folder2\\file4.sql').length).equal(1); - should(project.files.filter(f => f.relativePath === 'folder2\\file5.sql').length).equal(1); - - // - should(project.files.filter(f => f.relativePath === 'file1.sql').length).equal(0); - }); - it('Should exclude pre/post/none deploy scripts correctly', async function (): Promise { - const folderPath = await testUtils.generateTestFolderPath(); - projFilePath = await testUtils.createTestSqlProjFile(baselines.newSdkStyleProjectSdkNodeBaseline, folderPath); + const folderPath = await testUtils.generateTestFolderPath(this.test); + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.newSdkStyleProjectSdkNodeBaseline, folderPath); const project: Project = await Project.openProject(projFilePath); await project.addScriptItem('Script.PreDeployment1.sql', 'fake contents', ItemType.preDeployScript); @@ -1089,134 +333,40 @@ describe('Project: sdk style project content operations', function (): void { await project.addScriptItem('Script.PostDeployment1.sql', 'fake contents', ItemType.postDeployScript); // verify they were added to the sqlproj - let projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText.includes('')).equal(true); - should(projFileText.includes('')).equal(true); - should(projFileText.includes('')).equal(true); should(project.preDeployScripts.length).equal(1, 'Script.PreDeployment1.sql should have been added'); should(project.noneDeployScripts.length).equal(1, 'Script.PreDeployment2.sql should have been added'); should(project.preDeployScripts.length).equal(1, 'Script.PostDeployment1.sql should have been added'); - should(project.files.length).equal(0, 'There should not be any files'); + should(project.files.length).equal(0, 'There should not be any SQL object scripts'); // exclude the pre/post/none deploy script - await project.exclude(project.preDeployScripts.find(f => f.relativePath === 'Script.PreDeployment1.sql')!); - await project.exclude(project.noneDeployScripts.find(f => f.relativePath === 'Script.PreDeployment2.sql')!); - await project.exclude(project.postDeployScripts.find(f => f.relativePath === 'Script.PostDeployment1.sql')!); - - // verify they are excluded in the sqlproj - projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText.includes('')).equal(false); - should(projFileText.includes('')).equal(false); - should(projFileText.includes('')).equal(false); - should(projFileText.includes('')).equal(true); - should(projFileText.includes('')).equal(true); - should(projFileText.includes('')).equal(true); + await project.excludePreDeploymentScript('Script.PreDeployment1.sql'); + await project.excludeNoneItem('Script.PreDeployment2.sql'); + await project.excludePostDeploymentScript('Script.PostDeployment1.sql'); should(project.preDeployScripts.length).equal(0, 'Script.PreDeployment1.sql should have been removed'); should(project.noneDeployScripts.length).equal(0, 'Script.PreDeployment2.sql should have been removed'); should(project.postDeployScripts.length).equal(0, 'Script.PostDeployment1.sql should have been removed'); - should(project.files.length).equal(0, 'There should not be any files after the excludes'); + should(project.files.length).equal(0, 'There should not be any SQL object scripts after the excludes'); }); - it('Should handle excluding files included by glob patterns', async function (): Promise { - const testFolderPath = await testUtils.generateTestFolderPath(); - const mainProjectPath = path.join(testFolderPath, 'project'); - const otherFolderPath = path.join(testFolderPath, 'other'); - projFilePath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectWithGlobsSpecifiedBaseline, mainProjectPath); - await testUtils.createDummyFileStructure(false, undefined, path.dirname(projFilePath)); - - // create files outside of project folder that are included in the project file - await fs.mkdir(otherFolderPath); - await testUtils.createOtherDummyFiles(otherFolderPath); + // skipped because exclude folder not yet supported + it.skip('Should handle excluding glob included folders', async function (): Promise { + const testFolderPath = await testUtils.generateTestFolderPath(this.test); + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.openSdkStyleSqlProjectBaseline, testFolderPath); + await testUtils.createDummyFileStructureWithPrePostDeployScripts(this.test, false, undefined, path.dirname(projFilePath)); const project: Project = await Project.openProject(projFilePath); - should(project.files.filter(f => f.type === EntryType.File).length).equal(18); - - // exclude a file in the project's folder - should(project.files.filter(f => f.relativePath === 'folder1\\file1.sql').length).equal(1); - await project.exclude(project.files.find(f => f.relativePath === 'folder1\\file1.sql')!); - should(project.files.filter(f => f.relativePath === 'folder1\\file1.sql').length).equal(0); - - // exclude explicitly included file from an outside folder - should(project.files.filter(f => f.relativePath === '..\\other\\file1.sql').length).equal(1); - await project.exclude(project.files.find(f => f.relativePath === '..\\other\\file1.sql')!); - should(project.files.filter(f => f.relativePath === '..\\other\\file1.sql').length).equal(0); - - // exclude glob included file from an outside folder - should(project.files.filter(f => f.relativePath === '..\\other\\folder1\\test2.sql').length).equal(1); - await project.exclude(project.files.find(f => f.relativePath === '..\\other\\folder1\\test2.sql')!); - should(project.files.filter(f => f.relativePath === '..\\other\\folder1\\test2.sql').length).equal(0); - - // make sure a was added - const projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText.includes('')).equal(true, projFileText); - - // make sure was removed and no was added for it - should(projFileText.includes('')).equal(false, projFileText); - should(projFileText.includes('')).equal(false, projFileText); - - // make sure a was added - should(projFileText.includes('')).equal(true, projFileText); - }); - - it('Should only add Build entries to sqlproj for files not included by project folder glob and external streaming jobs', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectBaseline); - const project = await Project.openProject(projFilePath); - - const folderPath = 'Stored Procedures\\'; - const scriptPath = path.join(folderPath, 'Fake Stored Proc.sql'); - const scriptContents = 'SELECT \'This is not actually a stored procedure.\''; - - const scriptPathTagged = 'Fake External Streaming Job.sql'; - const scriptContentsTagged = 'EXEC sys.sp_create_streaming_job \'job\', \'SELECT 7\''; - - const outsideFolderScriptPath = path.join('..', 'Other Fake Stored Proc.sql'); - const outsideFolderScriptContents = 'SELECT \'This is also not actually a stored procedure.\''; - - const otherFolderPath = 'OtherFolder\\'; - - await project.addScriptItem(scriptPath, scriptContents); - await project.addScriptItem(scriptPathTagged, scriptContentsTagged, ItemType.externalStreamingJob); - await project.addScriptItem(outsideFolderScriptPath, outsideFolderScriptContents); - await project.addFolderItem(otherFolderPath); - - const newProject = await Project.openProject(projFilePath); - - should(newProject.files.find(f => f.type === EntryType.Folder && f.relativePath === convertSlashesForSqlProj(folderPath))).not.equal(undefined); - should(newProject.files.find(f => f.type === EntryType.File && f.relativePath === convertSlashesForSqlProj(scriptPath))).not.equal(undefined); - should(newProject.files.find(f => f.type === EntryType.File && f.relativePath === convertSlashesForSqlProj(scriptPathTagged))).not.equal(undefined); - should(newProject.files.find(f => f.type === EntryType.File && f.relativePath === convertSlashesForSqlProj(scriptPathTagged))?.sqlObjectType).equal(constants.ExternalStreamingJob); - should(newProject.files.find(f => f.type === EntryType.File && f.relativePath === convertSlashesForSqlProj(outsideFolderScriptPath))).not.equal(undefined); - - should(newProject.files.find(f => f.type === EntryType.Folder && f.relativePath === convertSlashesForSqlProj(otherFolderPath))).not.equal(undefined); - - // only the external streaming job and file outside of the project folder should have been added to the sqlproj - const projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText.includes('')).equal(false, projFileText); - should(projFileText.includes('')).equal(false, projFileText); - should(projFileText.includes('')).equal(true, projFileText); - should(projFileText.includes('')).equal(true, projFileText); - should(projFileText.includes('')).equal(false, projFileText); - }); - - it('Should handle excluding glob included folders', async function (): Promise { - const testFolderPath = await testUtils.generateTestFolderPath(); - projFilePath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectBaseline, testFolderPath); - await testUtils.createDummyFileStructureWithPrePostDeployScripts(false, undefined, path.dirname(projFilePath)); - - const project: Project = await Project.openProject(projFilePath); - - should(project.files.filter(f => f.type === EntryType.File).length).equal(13); - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(3); + should(project.files.length).equal(13); + should(project.folders.length).equal(3); should(project.noneDeployScripts.length).equal(2); // try to exclude a glob included folder - await project.exclude(project.files.find(f => f.relativePath === 'folder1\\')!); + //await project.excludeFolder('folder1\\'); // verify folder and contents are excluded - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(1); - should(project.files.filter(f => f.type === EntryType.File).length).equal(6); + should(project.folders.length).equal(1); + should(project.files.length).equal(6); should(project.noneDeployScripts.length).equal(1, 'Script.PostDeployment2.sql should have been excluded'); should(project.files.find(f => f.relativePath === 'folder1\\')).equal(undefined); @@ -1227,22 +377,23 @@ describe('Project: sdk style project content operations', function (): void { should(projFileText.includes('')).equal(false, projFileText); }); - it('Should handle excluding nested glob included folders', async function (): Promise { - const testFolderPath = await testUtils.generateTestFolderPath(); - projFilePath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectBaseline, testFolderPath); - await testUtils.createDummyFileStructureWithPrePostDeployScripts(false, undefined, path.dirname(projFilePath)); + // skipped because exclude folder not yet supported + it.skip('Should handle excluding nested glob included folders', async function (): Promise { + const testFolderPath = await testUtils.generateTestFolderPath(this.test,); + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.openSdkStyleSqlProjectBaseline, testFolderPath); + await testUtils.createDummyFileStructureWithPrePostDeployScripts(this.test, false, undefined, path.dirname(projFilePath)); const project: Project = await Project.openProject(projFilePath); - should(project.files.filter(f => f.type === EntryType.File).length).equal(13); - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(3); + should(project.files.length).equal(13); + should(project.folders.length).equal(3); // try to exclude a glob included folder - await project.exclude(project.files.find(f => f.relativePath === 'folder1\\nestedFolder\\')!); + //await project.excludeFolder('folder1\\nestedFolder\\'); // verify folder and contents are excluded - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(2); - should(project.files.filter(f => f.type === EntryType.File).length).equal(11); + should(project.folders.length).equal(2); + should(project.files.length).equal(11); should(project.files.find(f => f.relativePath === 'folder1\\nestedFolder\\')).equal(undefined); // verify sqlproj has glob exclude for folder, but not for files @@ -1251,32 +402,33 @@ describe('Project: sdk style project content operations', function (): void { should(projFileText.includes('')).equal(false, projFileText); }); - it('Should handle excluding explicitly included folders', async function (): Promise { - const testFolderPath = await testUtils.generateTestFolderPath(); - projFilePath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectWithFilesSpecifiedBaseline, testFolderPath); - await testUtils.createDummyFileStructure(false, undefined, path.dirname(projFilePath)); + // skipped because exclude folder not yet supported + it.skip('Should handle excluding explicitly included folders', async function (): Promise { + const testFolderPath = await testUtils.generateTestFolderPath(this.test,); + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.openSdkStyleSqlProjectWithFilesSpecifiedBaseline, testFolderPath); + await testUtils.createDummyFileStructure(this.test, false, undefined, path.dirname(projFilePath)); const project: Project = await Project.openProject(projFilePath); - should(project.files.filter(f => f.type === EntryType.File).length).equal(11); - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(2); + should(project.files.length).equal(11); + should(project.folders.length).equal(2); should(project.files.find(f => f.relativePath === 'folder1\\')!).not.equal(undefined); should(project.files.find(f => f.relativePath === 'folder2\\')!).not.equal(undefined); // try to exclude an explicitly included folder without trailing \ in sqlproj - await project.exclude(project.files.find(f => f.relativePath === 'folder1\\')!); + //await project.excludeFolder('folder1\\'); // verify folder and contents are excluded - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(1); - should(project.files.filter(f => f.type === EntryType.File).length).equal(6); + should(project.folders.length).equal(1); + should(project.files.length).equal(6); should(project.files.find(f => f.relativePath === 'folder1\\')).equal(undefined); // try to exclude an explicitly included folder with trailing \ in sqlproj - await project.exclude(project.files.find(f => f.relativePath === 'folder2\\')!); + //await project.excludeFolder('folder2\\'); // verify folder and contents are excluded - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(0); - should(project.files.filter(f => f.type === EntryType.File).length).equal(1); + should(project.folders.length).equal(0); + should(project.files.length).equal(1); should(project.files.find(f => f.relativePath === 'folder2\\')).equal(undefined); // make sure both folders are removed from sqlproj and remove entry is added @@ -1288,181 +440,33 @@ describe('Project: sdk style project content operations', function (): void { should(projFileText.includes('')).equal(true, projFileText); }); - it('Should handle adding empty folders and removing the Folder entry when the folder is no longer empty', async function (): Promise { - const testFolderPath = await testUtils.generateTestFolderPath(); - projFilePath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectWithFilesSpecifiedBaseline, testFolderPath); - await testUtils.createDummyFileStructure(false, undefined, path.dirname(projFilePath)); + // TODO: skipped until fix for folder trailing slashes comes in from DacFx + it.skip('Should handle deleting explicitly included folders', async function (): Promise { + const testFolderPath = await testUtils.generateTestFolderPath(this.test,); + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.openSdkStyleSqlProjectWithFilesSpecifiedBaseline, testFolderPath); + await testUtils.createDummyFileStructureWithPrePostDeployScripts(this.test, false, undefined, path.dirname(projFilePath)); const project: Project = await Project.openProject(projFilePath); - should(project.files.filter(f => f.type === EntryType.File).length).equal(11); - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(2); - should(project.files.find(f => f.relativePath === 'folder1\\')!).not.equal(undefined); - should(project.files.find(f => f.relativePath === 'folder2\\')!).not.equal(undefined); - - // try to add a new folder - await project.addFolderItem('folder3\\'); - - // try to add a new folder without trailing backslash - await project.addFolderItem('folder4'); - - // verify folders were added - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(4); - should(project.files.filter(f => f.type === EntryType.File).length).equal(11); - should(project.files.find(f => f.relativePath === 'folder3\\')).not.equal(undefined); - should(project.files.find(f => f.relativePath === 'folder4\\')).not.equal(undefined); - - // verify folders were added and the entries have a backslash in the sqlproj - let projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText.includes('')).equal(true, projFileText); - should(projFileText.includes('')).equal(true, projFileText); - - // add file to folder3 - await project.addScriptItem(path.join('folder3', 'test.sql'), 'fake contents'); - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(4); - should(project.files.filter(f => f.type === EntryType.File).length).equal(12); - should(project.files.find(f => f.relativePath === 'folder3\\test.sql')).not.equal(undefined); - - // verify folder3 entry is no longer in sqlproj - projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText.includes('')).equal(false, projFileText); - should(projFileText.includes('')).equal(true, projFileText); - }); - - it('Should handle adding nested empty folders', async function (): Promise { - const testFolderPath = await testUtils.generateTestFolderPath(); - projFilePath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectWithFilesSpecifiedBaseline, testFolderPath); - await testUtils.createDummyFileStructure(false, undefined, path.dirname(projFilePath)); - - let project: Project = await Project.openProject(projFilePath); - - should(project.files.filter(f => f.type === EntryType.File).length).equal(11); - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(2); - should(project.files.find(f => f.relativePath === 'folder1\\')!).not.equal(undefined); - should(project.files.find(f => f.relativePath === 'folder2\\')!).not.equal(undefined); - - // try to add a new folder - await project.addFolderItem('folder3\\'); - - // try to add a nested folder - await project.addFolderItem('folder3\\innerFolder\\'); - - // verify folders were added - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(4); - should(project.files.filter(f => f.type === EntryType.File).length).equal(11); - should(project.files.find(f => f.relativePath === 'folder3\\')).not.equal(undefined); - should(project.files.find(f => f.relativePath === 'folder3\\innerFolder\\')).not.equal(undefined); - - // verify there's only one folder entry for the two folders that were added - let projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText.includes('')).equal(false, projFileText); - should(projFileText.includes('')).equal(true, projFileText); - - // load the project again and make sure both new folders get loaded - project = await Project.openProject(projFilePath); - should(project.files.filter(f => f.type === EntryType.File).length).equal(11); - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(4); - should(project.files.find(f => f.relativePath === 'folder3\\')!).not.equal(undefined, 'folder3\\ should be loaded'); - should(project.files.find(f => f.relativePath === 'folder3\\innerFolder\\')!).not.equal(undefined, 'folder3\\innerFolder\\ should be loaded'); - - // add file to folder3 - await project.addScriptItem(path.join('folder3', 'test.sql'), 'fake contents'); - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(4); - should(project.files.filter(f => f.type === EntryType.File).length).equal(12); - should(project.files.find(f => f.relativePath === 'folder3\\test.sql')).not.equal(undefined, 'folder3\\test.sql should be in the project files'); - - // verify folder entry for innerFolder entry is still there - projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText.includes('')).equal(true, projFileText); - - // load the project again and make sure the folders still get loaded - project = await Project.openProject(projFilePath); - should(project.files.filter(f => f.type === EntryType.File).length).equal(12); - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(4); - should(project.files.find(f => f.relativePath === 'folder3\\')!).not.equal(undefined, 'folder3\\ should be loaded'); - should(project.files.find(f => f.relativePath === 'folder3\\innerFolder\\')!).not.equal(undefined, 'folder3\\innerFolder\\ should be loaded'); - }); - - it('Should handle deleting empty folders', async function (): Promise { - const testFolderPath = await testUtils.generateTestFolderPath(); - projFilePath = await testUtils.createTestSqlProjFile(baselines.newSdkStyleProjectSdkNodeBaseline, testFolderPath); - - const project: Project = await Project.openProject(projFilePath); - const beforeProjFileText = (await fs.readFile(projFilePath)).toString(); - - should(project.files.filter(f => f.type === EntryType.File).length).equal(0); - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(0); - - // add an empty folder - await project.addFolderItem('folder1'); - - // verify folder was added - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(1); - should(project.files.filter(f => f.type === EntryType.File).length).equal(0); - should(project.files.find(f => f.relativePath === 'folder1\\')).not.equal(undefined, 'folder1 should have been added'); - - // verify entry was added for the new empty folder in the sqlproj - let projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText.includes('')).equal(true, projFileText); - - // delete the empty folder - await project.deleteFileFolder(project.files.find(f => f.relativePath === 'folder1\\')!); - - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(0, 'folder1 should have been deleted'); - - // verify the folder entry was removed from the sqlproj and a Build Remove was not added - projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText.trimEnd() === beforeProjFileText.trimEnd()).equal(true, 'The sqlproj should not have changed after deleting folder1'); - }); - - it('Should handle deleting not empty glob included folders', async function (): Promise { - const testFolderPath = await testUtils.generateTestFolderPath(); - projFilePath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectBaseline, testFolderPath); - await testUtils.createDummyFileStructureWithPrePostDeployScripts(false, undefined, path.dirname(projFilePath)); - - const project: Project = await Project.openProject(projFilePath); - const beforeProjFileText = (await fs.readFile(projFilePath)).toString(); - - should(project.files.filter(f => f.type === EntryType.File).length).equal(13); - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(3); - - // delete a folder with contents - await project.deleteFileFolder(project.files.find(f => f.relativePath === 'folder2\\')!); - - should(project.files.filter(f => f.type === EntryType.File).length).equal(8); - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(2); - - // verify the folder entry was removed from the sqlproj and a Build Remove was not added - const projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText.trimEnd() === beforeProjFileText.trimEnd()).equal(true, 'The sqlproj should not have changed after deleting folder2'); - }); - - it('Should handle deleting explicitly included folders', async function (): Promise { - const testFolderPath = await testUtils.generateTestFolderPath(); - projFilePath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectWithFilesSpecifiedBaseline, testFolderPath); - await testUtils.createDummyFileStructureWithPrePostDeployScripts(false, undefined, path.dirname(projFilePath)); - - const project: Project = await Project.openProject(projFilePath); - - should(project.files.filter(f => f.type === EntryType.File).length).equal(13); - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(3); + should(project.files.length).equal(13); + should(project.folders.length).equal(3); should(project.files.find(f => f.relativePath === 'folder1\\')!).not.equal(undefined); should(project.files.find(f => f.relativePath === 'folder2\\')!).not.equal(undefined); // try to delete an explicitly included folder with the trailing \ in sqlproj - await project.deleteFileFolder(project.files.find(f => f.relativePath === 'folder2\\')!); + await project.deleteFolder('folder2\\'); // verify the project not longer has folder2 and its contents - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(2); - should(project.files.filter(f => f.type === EntryType.File).length).equal(8); + should(project.folders.length).equal(2); + should(project.files.length).equal(8); should(project.files.find(f => f.relativePath === 'folder2\\')).equal(undefined); // try to delete an explicitly included folder without trailing \ in sqlproj - await project.deleteFileFolder(project.files.find(f => f.relativePath === 'folder1\\')!); + await project.deleteFolder('folder1\\'); // verify the project not longer has folder1 and its contents - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(0); - should(project.files.filter(f => f.type === EntryType.File).length).equal(1); + should(project.folders.length).equal(0); + should(project.files.length).equal(1); should(project.files.find(f => f.relativePath === 'folder1\\')).equal(undefined); // make sure both folders are removed from sqlproj and Build Remove entries were not added @@ -1474,23 +478,9 @@ describe('Project: sdk style project content operations', function (): void { should(projFileText.includes('')).equal(false, projFileText); }); - it('Should add a project guid if there is not one in the sqlproj', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectNoProjectGuidBaseline); - let projFileText = (await fs.readFile(projFilePath)).toString(); - - // verify no project guid - should(projFileText.includes(constants.ProjectGuid)).equal(false); - - const project: Project = await Project.openProject(projFilePath); - - // verify project guid was added - projFileText = (await fs.readFile(projFilePath)).toString(); - should(project.projectGuid).not.equal(undefined); - should(projFileText.includes(constants.ProjectGuid)).equal(true); - }); - + // TODO: remove once DacFx exposes both absolute and relative outputPath it('Should read OutputPath from sqlproj if there is one for SDK-style project', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectBaseline); + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.openSdkStyleSqlProjectBaseline); const projFileText = (await fs.readFile(projFilePath)).toString(); // Verify sqlproj has OutputPath @@ -1501,71 +491,350 @@ describe('Project: sdk style project content operations', function (): void { should(project.dacpacOutputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath('..\\otherFolder'), `${project.projectFileName}.dacpac`)); }); + // TODO: move test to DacFx it('Should use default output path if OutputPath is not specified in sqlproj', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectWithGlobsSpecifiedBaseline); + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.openSdkStyleSqlProjectWithGlobsSpecifiedBaseline); const projFileText = (await fs.readFile(projFilePath)).toString(); - // Verify sqlproj doesn't have OutputPath - should(projFileText.includes(constants.OutputPath)).equal(true); + // Verify sqlproj doesn't have + should(projFileText.includes(`<${constants.OutputPath}>`)).equal(false); const project: Project = await Project.openProject(projFilePath); - should(project.outputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath(constants.defaultOutputPath(project.configuration.toString())))); + should(project.outputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath(constants.defaultOutputPath(project.configuration.toString()))) + path.sep); should(project.dacpacOutputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath(constants.defaultOutputPath(project.configuration.toString())), `${project.projectFileName}.dacpac`)); }); +}); - it('Should handle adding existing items to project', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectBaseline); - const projectFolder = path.dirname(projFilePath); +describe('Project: database references', function (): void { + before(async function (): Promise { + await baselines.loadBaselines(); + }); - // Create a sql file inside project root - const sqlFile = path.join(projectFolder, 'test.sql'); - await fs.writeFile(sqlFile, ''); + after(async function (): Promise { + await testUtils.deleteGeneratedTestFolder(); + }); - const project: Project = await Project.openProject(projFilePath); + it('Should read database references correctly', async function (): Promise { + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.databaseReferencesReadBaseline); + const project = await Project.openProject(projFilePath); + (project.databaseReferences.length).should.equal(5, 'NUmber of database references'); - // Add it as existing file - await project.addExistingItem(sqlFile); + const systemRef: SystemDatabaseReferenceProjectEntry | undefined = project.databaseReferences.find(r => r instanceof SystemDatabaseReferenceProjectEntry) as SystemDatabaseReferenceProjectEntry; + should(systemRef).not.equal(undefined, 'msdb reference'); + (systemRef!.referenceName).should.equal(constants.msdb); + (systemRef!.databaseVariableLiteralValue!).should.equal('msdbLiteral'); + (systemRef!.suppressMissingDependenciesErrors).should.equal(true, 'suppressMissingDependenciesErrors for system db'); - // Validate it has been added to project - should(project.files.length).equal(1, 'Only one entry is expected in the project'); - const sqlFileEntry = project.files.find(f => f.type === EntryType.File && f.relativePath === 'test.sql'); - should(sqlFileEntry).not.equal(undefined); + let projRef: SqlProjectReferenceProjectEntry | undefined = project.databaseReferences.find(r => r instanceof SqlProjectReferenceProjectEntry && r.referenceName === 'ReferencedProject') as SqlProjectReferenceProjectEntry; + should(projRef).not.equal(undefined, 'ReferencedProject reference'); + (projRef!.pathForSqlProj()).should.equal('..\\ReferencedProject\\ReferencedProject.sqlproj'); + (projRef!.projectGuid).should.equal('{BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575}'); + should(projRef!.databaseVariableLiteralValue).equal(null, 'databaseVariableLiteralValue for ReferencedProject'); + (projRef!.databaseSqlCmdVariableName!).should.equal('projDbVar'); + (projRef!.databaseSqlCmdVariableValue!).should.equal('$(SqlCmdVar__1)'); + (projRef!.serverSqlCmdVariableName!).should.equal('projServerVar'); + (projRef!.serverSqlCmdVariableValue!).should.equal('$(SqlCmdVar__2)'); + (projRef!.suppressMissingDependenciesErrors).should.equal(true, 'suppressMissingDependenciesErrors for ReferencedProject'); - // Validate project XML should not have changed as the file falls under default glob + projRef = project.databaseReferences.find(r => r instanceof SqlProjectReferenceProjectEntry && r.referenceName === 'OtherProject') as SqlProjectReferenceProjectEntry; + should(projRef).not.equal(undefined, 'OtherProject reference'); + (projRef!.pathForSqlProj()).should.equal('..\\OtherProject\\OtherProject.sqlproj'); + (projRef!.projectGuid).should.equal('{C0DEBA11-BA5E-5EA7-ACE5-BABB1E70A575}'); + (projRef!.databaseVariableLiteralValue!).should.equal('OtherProjLiteral', 'databaseVariableLiteralValue for OtherProject'); + should(projRef!.databaseSqlCmdVariableName).equal(undefined); + should(projRef!.databaseSqlCmdVariableValue).equal(undefined); + should(projRef!.serverSqlCmdVariableName).equal(undefined); + should(projRef!.serverSqlCmdVariableValue).equal(undefined); + (projRef!.suppressMissingDependenciesErrors).should.equal(false, 'suppressMissingDependenciesErrors for OtherProject'); + + let dacpacRef: DacpacReferenceProjectEntry | undefined = project.databaseReferences.find(r => r instanceof DacpacReferenceProjectEntry && r.referenceName === 'ReferencedDacpac') as DacpacReferenceProjectEntry; + should(dacpacRef).not.equal(undefined, 'dacpac reference for ReferencedDacpac'); + (dacpacRef!.pathForSqlProj()).should.equal('..\\ReferencedDacpac\\ReferencedDacpac.dacpac'); + should(dacpacRef!.databaseVariableLiteralValue).equal(null, 'databaseVariableLiteralValue for ReferencedDacpac'); + (dacpacRef!.databaseSqlCmdVariableName!).should.equal('dacpacDbVar'); + (dacpacRef!.databaseSqlCmdVariableValue!).should.equal('$(SqlCmdVar__3)'); + (dacpacRef!.serverSqlCmdVariableName!).should.equal('dacpacServerVar'); + (dacpacRef!.serverSqlCmdVariableValue!).should.equal('$(SqlCmdVar__4)'); + (dacpacRef!.suppressMissingDependenciesErrors).should.equal(false, 'suppressMissingDependenciesErrors for ReferencedDacpac'); + + dacpacRef = project.databaseReferences.find(r => r instanceof DacpacReferenceProjectEntry && r.referenceName === 'OtherDacpac') as DacpacReferenceProjectEntry; + should(dacpacRef).not.equal(undefined, 'dacpac reference for OtherDacpac'); + (dacpacRef!.pathForSqlProj()).should.equal('..\\OtherDacpac\\OtherDacpac.dacpac'); + (dacpacRef!.databaseVariableLiteralValue!).should.equal('OtherDacpacLiteral', 'databaseVariableLiteralValue for OtherDacpac'); + should(dacpacRef!.databaseSqlCmdVariableName).equal(undefined); + should(dacpacRef!.databaseSqlCmdVariableValue).equal(undefined); + should(dacpacRef!.serverSqlCmdVariableName).equal(undefined); + should(dacpacRef!.serverSqlCmdVariableValue).equal(undefined); + (dacpacRef!.suppressMissingDependenciesErrors).should.equal(true, 'suppressMissingDependenciesErrors for OtherDacpac'); + }); + + it('Should delete database references correctly', async function (): Promise { + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.databaseReferencesReadBaseline); + const project = await Project.openProject(projFilePath); + + (project.databaseReferences.length).should.equal(5, 'There should be five database references'); + + await project.deleteDatabaseReference(constants.msdb); + (project.databaseReferences.length).should.equal(4, 'There should be four database references after deletion'); + + let ref = project.databaseReferences.find(r => r.referenceName === constants.msdb); + should(ref).equal(undefined, 'msdb reference should be deleted'); + }); + + it('Should add system database reference correctly', async function (): Promise { + let project = await testUtils.createTestSqlProject(this.test); + + const msdbRefSettings: ISystemDatabaseReferenceSettings = { databaseVariableLiteralValue: systemDatabaseToString(SystemDatabase.MSDB), systemDb: SystemDatabase.MSDB, suppressMissingDependenciesErrors: true }; + await project.addSystemDatabaseReference(msdbRefSettings); + + (project.databaseReferences.length).should.equal(1, 'There should be one database reference after adding a reference to msdb'); + (project.databaseReferences[0].referenceName).should.equal(msdbRefSettings.databaseVariableLiteralValue, 'databaseName'); + (project.databaseReferences[0].suppressMissingDependenciesErrors).should.equal(msdbRefSettings.suppressMissingDependenciesErrors, 'suppressMissingDependenciesErrors'); + }); + + it('Should add a dacpac reference to the same database correctly', async function (): Promise { + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.newProjectFileBaseline); + let project = await Project.openProject(projFilePath); + + // add database reference in the same database + should(project.databaseReferences.length).equal(0, 'There should be no database references to start with'); + await project.addDatabaseReference({ dacpacFileLocation: Uri.file('test1.dacpac'), suppressMissingDependenciesErrors: true }); + + should(project.databaseReferences.length).equal(1, 'There should be a database reference after adding a reference to test1'); + should(project.databaseReferences[0].referenceName).equal('test1', 'The database reference should be test1'); + should(project.databaseReferences[0].suppressMissingDependenciesErrors).equal(true, 'project.databaseReferences[0].suppressMissingDependenciesErrors should be true'); + }); + + it('Should add a dacpac reference to a different database in the same server correctly', async function (): Promise { + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.newProjectFileBaseline); + const project = await Project.openProject(projFilePath); + + // add database reference to a different database on the same server + should(project.databaseReferences.length).equal(0, 'There should be no database references to start with'); + await project.addDatabaseReference({ + dacpacFileLocation: Uri.file('test2.dacpac'), + databaseName: 'test2DbName', + databaseVariable: 'test2Db', + suppressMissingDependenciesErrors: false + }); + should(project.databaseReferences.length).equal(1, 'There should be a database reference after adding a reference to test2'); + should(project.databaseReferences[0].referenceName).equal('test2', 'The database reference should be test2'); + should(project.databaseReferences[0].suppressMissingDependenciesErrors).equal(false, 'project.databaseReferences[0].suppressMissingDependenciesErrors should be false'); + }); + + it('Should add a dacpac reference to a different database in a different server correctly', async function (): Promise { + const projFilePath = await testUtils.createTestSqlProjFile(this.test, 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'); + await project.addDatabaseReference({ + dacpacFileLocation: Uri.file('test3.dacpac'), + databaseName: 'test3DbName', + databaseVariable: 'test3Db', + serverName: 'otherServerName', + serverVariable: 'otherServer', + suppressMissingDependenciesErrors: false + }); + should(project.databaseReferences.length).equal(1, 'There should be a database reference after adding a reference to test3'); + should(project.databaseReferences[0].referenceName).equal('test3', 'The database reference should be test3'); + should(project.databaseReferences[0].suppressMissingDependenciesErrors).equal(false, 'project.databaseReferences[0].suppressMissingDependenciesErrors should be false'); + }); + + it('Should add a project reference to the same database correctly', async function (): Promise { + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.newProjectFileBaseline); + let 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].referenceName).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}`); + }); + + it('Should add a project reference to a different database in the same server correctly', async function (): Promise { + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.newProjectFileBaseline); + let 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].referenceName).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}`); + }); + + it('Should add a project reference to a different database in a different server correctly', async function (): Promise { + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.newProjectFileBaseline); + let 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].referenceName).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}`); + }); + + it('Should not allow adding duplicate dacpac references', async function (): Promise { + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.newProjectFileBaseline); + let project = await Project.openProject(projFilePath); + + should(project.databaseReferences.length).equal(0, 'There should be no database references to start with'); + + const dacpacReference: IDacpacReferenceSettings = { dacpacFileLocation: Uri.file('test.dacpac'), suppressMissingDependenciesErrors: false }; + await project.addDatabaseReference(dacpacReference); + + should(project.databaseReferences.length).equal(1, 'There should be one database reference after adding a reference to test.dacpac'); + should(project.databaseReferences[0].referenceName).equal('test', 'project.databaseReferences[0].databaseName should be test'); + + // try to add reference to test.dacpac again + await testUtils.shouldThrowSpecificError(async () => await project.addDatabaseReference(dacpacReference), constants.databaseReferenceAlreadyExists); + should(project.databaseReferences.length).equal(1, 'There should be one database reference after trying to add a reference to test.dacpac again'); + }); + + it('Should not allow adding duplicate system database references', async function (): Promise { + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.newProjectFileBaseline); + let project = await Project.openProject(projFilePath); + + should(project.databaseReferences.length).equal(0, 'There should be no database references to start with'); + + const systemDbReference: ISystemDatabaseReferenceSettings = { databaseVariableLiteralValue: systemDatabaseToString(SystemDatabase.Master), systemDb: SystemDatabase.Master, suppressMissingDependenciesErrors: false }; + await project.addSystemDatabaseReference(systemDbReference); + project = await Project.openProject(projFilePath); + should(project.databaseReferences.length).equal(1, 'There should be one database reference after adding a reference to master'); + should(project.databaseReferences[0].referenceName).equal(constants.master, 'project.databaseReferences[0].databaseName should be master'); + + // try to add reference to master again + await testUtils.shouldThrowSpecificError(async () => await project.addSystemDatabaseReference(systemDbReference), constants.databaseReferenceAlreadyExists); + should(project.databaseReferences.length).equal(1, 'There should only be one database reference after trying to add a reference to master again'); + }); + + it('Should not allow adding duplicate project references', async function (): Promise { + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.newProjectFileBaseline); + let project = await Project.openProject(projFilePath); + + should(project.databaseReferences.length).equal(0, 'There should be no database references to start with'); + + const projectReference: IProjectReferenceSettings = { + projectName: 'testProject', + projectGuid: '', + projectRelativePath: Uri.file('testProject.sqlproj'), + suppressMissingDependenciesErrors: false + }; + await project.addProjectReference(projectReference); + + should(project.databaseReferences.length).equal(1, 'There should be one database reference after adding a reference to testProject.sqlproj'); + should(project.databaseReferences[0].referenceName).equal('testProject', 'project.databaseReferences[0].databaseName should be testProject'); + + // try to add reference to testProject again + await testUtils.shouldThrowSpecificError(async () => await project.addProjectReference(projectReference), constants.databaseReferenceAlreadyExists); + should(project.databaseReferences.length).equal(1, 'There should be one database reference after trying to add a reference to testProject again'); + }); + + it('Should handle trying to add duplicate database references when slashes are different direction', async function (): Promise { + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.newProjectFileBaseline); + let project = await Project.openProject(projFilePath); + + should(project.databaseReferences.length).equal(0, 'There should be no database references to start with'); + + const projectReference: IProjectReferenceSettings = { + projectName: 'testProject', + projectGuid: '', + projectRelativePath: Uri.file('testFolder/testProject.sqlproj'), + suppressMissingDependenciesErrors: false + }; + await project.addProjectReference(projectReference); + + should(project.databaseReferences.length).equal(1, 'There should be one database reference after adding a reference to testProject.sqlproj'); + should(project.databaseReferences[0].referenceName).equal('testProject', 'project.databaseReferences[0].databaseName should be testProject'); + + // try to add reference to testProject again with slashes in the other direction + projectReference.projectRelativePath = Uri.file('testFolder\\testProject.sqlproj'); + await testUtils.shouldThrowSpecificError(async () => await project.addProjectReference(projectReference), constants.databaseReferenceAlreadyExists); + should(project.databaseReferences.length).equal(1, 'There should be one database reference after trying to add a reference to testProject again'); + }); + + it.skip('Should update sqlcmd variable values if value changes', async function (): Promise { + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.newProjectFileBaseline); + const project = await Project.openProject(projFilePath); + const databaseVariable = 'test3Db'; + const serverVariable = 'otherServer'; + + should(project.databaseReferences.length).equal(0, 'There should be no database references to start with'); + await project.addDatabaseReference({ + dacpacFileLocation: Uri.file('test3.dacpac'), + databaseName: 'test3DbName', + databaseVariable: databaseVariable, + serverName: 'otherServerName', + serverVariable: serverVariable, + suppressMissingDependenciesErrors: false + }); + should(project.databaseReferences.length).equal(1, 'There should be a database reference after adding a reference to test3'); + should(project.databaseReferences[0].referenceName).equal('test3', 'The database reference should be test3'); + should(Object.keys(project.sqlCmdVariables).length).equal(2, 'There should be 2 sqlcmdvars after adding the dacpac reference'); + + // make sure reference to test3.dacpac and SQLCMD variables were added let projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText.includes('')).equal(false, projFileText); + should(projFileText).containEql(''); + should(projFileText).containEql('test3DbName'); + should(projFileText).containEql(''); + should(projFileText).containEql('otherServerName'); + + // delete reference + await project.deleteDatabaseReferenceByEntry(project.databaseReferences[0]); + should(project.databaseReferences.length).equal(0, 'There should be no database references after deleting'); + should(Object.keys(project.sqlCmdVariables).length).equal(2, 'There should still be 2 sqlcmdvars after deleting the dacpac reference'); + + // add reference to the same dacpac again but with different values for the sqlcmd variables + await project.addDatabaseReference({ + dacpacFileLocation: Uri.file('test3.dacpac'), + databaseName: 'newDbName', + databaseVariable: databaseVariable, + serverName: 'newServerName', + serverVariable: serverVariable, + suppressMissingDependenciesErrors: false + }); + should(project.databaseReferences.length).equal(1, 'There should be a database reference after adding a reference to test3'); + should(project.databaseReferences[0].referenceName).equal('test3', 'The database reference should be test3'); + should(Object.keys(project.sqlCmdVariables).length).equal(2, 'There should still be 2 sqlcmdvars after adding the dacpac reference again with different sqlcmdvar values'); - // Exclude this file, verify the is added - await project.exclude(sqlFileEntry!); - should(project.files.length).equal(0, 'Project should not have any files remaining.'); projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText.includes('')).equal(true, projFileText); - - // Add the file back, verify the is no longer there - await project.addExistingItem(sqlFile); - projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText.includes('')).equal(false, projFileText); - should(projFileText.includes('')).equal(false, projFileText); - - // Now create a txt file and add it to sqlproj - const txtFile = path.join(projectFolder, 'test.txt'); - await fs.writeFile(txtFile, ''); - await project.addExistingItem(txtFile); - - // Validate the txt file is added as - should(project.files.find(f => f.type === EntryType.File && f.relativePath === 'test.txt')).not.equal(undefined); - projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText.includes('')).equal(true, projFileText); - - // Test with a sql file that's outside project root - const externalSqlFile = path.join(os.tmpdir(), `Test_${new Date().getTime()}.sql`); - const externalFileRelativePath = convertSlashesForSqlProj(path.relative(projectFolder, externalSqlFile)); - await fs.writeFile(externalSqlFile, ''); - await project.addExistingItem(externalSqlFile); - should(project.files.find(f => f.type === EntryType.File && f.relativePath === externalFileRelativePath)).not.equal(undefined); - projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText.includes(``)).equal(true, projFileText); - await fs.rm(externalSqlFile); + should(projFileText).containEql(''); + should(projFileText).containEql('newDbName'); + should(projFileText).containEql(''); + should(projFileText).containEql('newServerName'); }); }); @@ -1579,26 +848,23 @@ describe('Project: add SQLCMD Variables', function (): void { }); it('Should update .sqlproj with new sqlcmd variables', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline); - const project = await Project.openProject(projFilePath); - should(Object.keys(project.sqlCmdVariables).length).equal(2); + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.openProjectFileBaseline); + let project = await Project.openProject(projFilePath); + should(Object.keys(project.sqlCmdVariables).length).equal(2, 'The project should have 2 sqlcmd variables when opened'); // add a new variable await project.addSqlCmdVariable('TestDatabaseName', 'TestDb'); - // add a variable with the same name as an existing sqlcmd variable and the old entry should be replaced with the new one - await project.addSqlCmdVariable('ProdDatabaseName', 'NewProdName'); + // update value of an existing sqlcmd variable + await project.updateSqlCmdVariable('ProdDatabaseName', 'NewProdName'); - should(Object.keys(project.sqlCmdVariables).length).equal(3); - should(project.sqlCmdVariables['TestDatabaseName']).equal('TestDb'); + should(Object.keys(project.sqlCmdVariables).length).equal(3, 'There should be 3 sqlcmd variables after adding TestDatabaseName'); + should(project.sqlCmdVariables['TestDatabaseName']).equal('TestDb', 'Value of TestDatabaseName should be TestDb'); should(project.sqlCmdVariables['ProdDatabaseName']).equal('NewProdName', 'ProdDatabaseName value should have been updated to the new value'); - - const projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText).equal(baselines.openSqlProjectWithAdditionalSqlCmdVariablesBaseline.trim()); }); }); -describe('Project: add publish profiles', function (): void { +describe('Project: publish profiles', function (): void { before(async function (): Promise { await baselines.loadBaselines(); }); @@ -1607,18 +873,18 @@ describe('Project: add publish profiles', function (): void { await testUtils.deleteGeneratedTestFolder(); }); - it('Should update .sqlproj with new publish profiles', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline); + it('Should add new publish profile', async function (): Promise { + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.openProjectFileBaseline); const project = await Project.openProject(projFilePath); - should(Object.keys(project.publishProfiles).length).equal(3); + should(project.publishProfiles.length).equal(3); // add a new publish profile - await project.addPublishProfileToProjFile(path.join(projFilePath, 'TestProjectName_4.publish.xml')); + const newProfilePath = path.join(project.projectFolderPath, 'TestProjectName_4.publish.xml'); + await fs.writeFile(newProfilePath, ''); - should(Object.keys(project.publishProfiles).length).equal(4); + await project.addNoneItem('TestProjectName_4.publish.xml'); - const projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText).equal(baselines.openSqlProjectWithAdditionalPublishProfileBaseline.trim()); + should(project.publishProfiles.length).equal(4); }); }); @@ -1632,63 +898,70 @@ describe('Project: properties', function (): void { }); it('Should read target database version', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline); + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.openProjectFileBaseline); const project = await Project.openProject(projFilePath); should(project.getProjectTargetVersion()).equal('150'); }); it('Should throw on missing target database version', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.sqlProjectMissingVersionBaseline); - const project = await Project.openProject(projFilePath); + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.sqlProjectMissingVersionBaseline); - should(() => project.getProjectTargetVersion()).throw('Invalid DSP in .sqlproj file'); + await testUtils.shouldThrowSpecificError(async () => await Project.openProject(projFilePath), 'Error: No target platform defined. Missing node.'); }); it('Should throw on invalid target database version', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.sqlProjectInvalidVersionBaseline); - const project = await Project.openProject(projFilePath); + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.sqlProjectInvalidVersionBaseline); - should(() => project.getProjectTargetVersion()).throw('Invalid DSP in .sqlproj file'); + try { + await Project.openProject(projFilePath); + throw new Error('Should not have succeeded.'); + } catch (e) { + (e.message).should.startWith('Error: Invalid value for Database Schema Provider:'); + (e.message).should.endWith('expected to be in the form Microsoft.Data.Tools.Schema.Sql.Sql160DatabaseSchemaProvider'); + } }); it('Should read default database collation', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.sqlProjectCustomCollationBaseline); + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.sqlProjectCustomCollationBaseline); const project = await Project.openProject(projFilePath); should(project.getDatabaseDefaultCollation()).equal('SQL_Latin1_General_CP1255_CS_AS'); }); it('Should return default value when database collation is not specified', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.newProjectFileBaseline); const project = await Project.openProject(projFilePath); should(project.getDatabaseDefaultCollation()).equal('SQL_Latin1_General_CP1_CI_AS'); }); - it('Should throw on invalid default database collation', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.sqlProjectInvalidCollationBaseline); - const project = await Project.openProject(projFilePath); + // TODO: skipped until DacFx throws on invalid value + it.skip('Should throw on invalid default database collation', async function (): Promise { + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.sqlProjectInvalidCollationBaseline); - should(() => project.getDatabaseDefaultCollation()) - .throw('Invalid value specified for the property \'DefaultCollation\' in .sqlproj file'); + try { + await Project.openProject(projFilePath); + throw new Error('Should not have succeeded.'); + } catch (e) { + (e.message).should.startWith('Error: Invalid value for DefaultCollation:'); + } }); it('Should add database source to project property', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.sqlProjectInvalidCollationBaseline); - const project = await Project.openProject(projFilePath); + const project = await testUtils.createTestSqlProject(this.test); // Should add a single database source await project.addDatabaseSource('test1'); let databaseSourceItems: string[] = project.getDatabaseSourceValues(); - should(databaseSourceItems.length).equal(1); + should(databaseSourceItems.length).equal(1, 'number of database sources: ' + databaseSourceItems); should(databaseSourceItems[0]).equal('test1'); // Should add multiple database sources await project.addDatabaseSource('test2'); await project.addDatabaseSource('test3'); databaseSourceItems = project.getDatabaseSourceValues(); - should(databaseSourceItems.length).equal(3); + should(databaseSourceItems.length).equal(3, 'number of database sources: ' + databaseSourceItems); should(databaseSourceItems[0]).equal('test1'); should(databaseSourceItems[1]).equal('test2'); should(databaseSourceItems[2]).equal('test3'); @@ -1704,7 +977,7 @@ describe('Project: properties', function (): void { }); it('Should remove database source from project property', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.sqlProjectInvalidCollationBaseline); + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.sqlProjectInvalidCollationBaseline); const project = await Project.openProject(projFilePath); await project.addDatabaseSource('test1'); @@ -1731,49 +1004,8 @@ describe('Project: properties', function (): void { should(databaseSourceItems.length).equal(0); }); - it('Should add and remove values from project properties according to specified case sensitivity', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.sqlProjectInvalidCollationBaseline); - const project = await Project.openProject(projFilePath); - const propertyName = 'TestProperty'; - - // Should add value to collection - await project['addValueToCollectionProjectProperty'](propertyName, 'test'); - should(project['evaluateProjectPropertyValue'](propertyName)).equal('test'); - - // Should not allow duplicates of different cases when comparing case insitively - await project['addValueToCollectionProjectProperty'](propertyName, 'TEST'); - should(project['evaluateProjectPropertyValue'](propertyName)).equal('test'); - - // Should allow duplicates of differnt cases when comparing case sensitively - await project['addValueToCollectionProjectProperty'](propertyName, 'TEST', true); - should(project['evaluateProjectPropertyValue'](propertyName)).equal('test;TEST'); - - // Should remove values case insesitively - await project['removeValueFromCollectionProjectProperty'](propertyName, 'Test'); - should(project['evaluateProjectPropertyValue'](propertyName)).equal('TEST'); - - // Should remove values case sensitively - await project['removeValueFromCollectionProjectProperty'](propertyName, 'Test', true); - should(project['evaluateProjectPropertyValue'](propertyName)).equal('TEST'); - await project['removeValueFromCollectionProjectProperty'](propertyName, 'TEST', true); - should(project['evaluateProjectPropertyValue'](propertyName)).equal(undefined); - }); - - it('Should only return well known database strings when getWellKnownDatabaseSourceString function is called', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.sqlProjectInvalidCollationBaseline); - const project = await Project.openProject(projFilePath); - - await project.addDatabaseSource('test1'); - await project.addDatabaseSource('test2'); - await project.addDatabaseSource('test3'); - await project.addDatabaseSource(constants.WellKnownDatabaseSources[0]); - - should(getWellKnownDatabaseSources(project.getDatabaseSourceValues()).length).equal(1); - should(getWellKnownDatabaseSources(project.getDatabaseSourceValues())[0]).equal(constants.WellKnownDatabaseSources[0]); - }); - it('Should throw error when adding or removing database source that contains semicolon', async function (): Promise { - projFilePath = await testUtils.createTestSqlProjFile(baselines.sqlProjectInvalidCollationBaseline); + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.sqlProjectInvalidCollationBaseline); const project = await Project.openProject(projFilePath); const semicolon = ';'; @@ -1801,274 +1033,85 @@ describe('Project: round trip updates', function (): void { }); it('Should update SSDT project to work in ADS', async function (): Promise { - await testUpdateInRoundTrip(baselines.SSDTProjectFileBaseline, baselines.SSDTProjectAfterUpdateBaseline); + await testUpdateInRoundTrip(this.test, baselines.SSDTProjectFileBaseline); }); - it('Should update SSDT project with new system database references', async function (): Promise { - await testUpdateInRoundTrip(baselines.SSDTUpdatedProjectBaseline, baselines.SSDTUpdatedProjectAfterSystemDbUpdateBaseline); + // skipped until https://mssqltools.visualstudio.com/SQL%20Tools%20Semester%20Work%20Tracking/_workitems/edit/15749 is fixed + it.skip('Should update SSDT project with new system database references', async function (): Promise { + await testUpdateInRoundTrip(this.test, baselines.SSDTUpdatedProjectBaseline); }); it('Should update SSDT project to work in ADS handling pre-existing targets', async function (): Promise { - await testUpdateInRoundTrip(baselines.SSDTProjectBaselineWithBeforeBuildTarget, baselines.SSDTProjectBaselineWithBeforeBuildTargetAfterUpdate); + await testUpdateInRoundTrip(this.test, baselines.SSDTProjectBaselineWithBeforeBuildTarget); }); - it('Should not update project and no backup file should be created when update to project is rejected', async function (): Promise { + it('Should not update project and no backup file should be created when prompt to update project is rejected', async function (): Promise { sinon.stub(window, 'showWarningMessage').returns(Promise.resolve(constants.noString)); // setup test files - const folderPath = await testUtils.generateTestFolderPath(); - const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.SSDTProjectFileBaseline, folderPath); - await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath); + const folderPath = await testUtils.generateTestFolderPath(this.test); + const sqlProjPath = await testUtils.createTestSqlProjFile(this.test, baselines.SSDTProjectFileBaseline, folderPath); - const project = await Project.openProject(Uri.file(sqlProjPath).fsPath); + const originalSqlProjContents = (await fs.readFile(sqlProjPath)).toString(); - should(await exists(sqlProjPath + '_backup')).equal(false); // backup file should not be generated - should(project.importedTargets.length).equal(2); // additional target should not be added by updateProjectForRoundTrip method + // validate original state + let project = await Project.openProject(sqlProjPath, false); + (project.isCrossPlatformCompatible).should.be.false('SSDT project should not be cross-platform compatible when not prompted to update'); + + // validate rejection result + project = await Project.openProject(sqlProjPath, true); + (project.isCrossPlatformCompatible).should.be.false('SSDT project should not be cross-platform compatible when update prompt is rejected'); + (await exists(sqlProjPath + '_backup')).should.be.false('backup file should not be generated'); + + const newSqlProjContents = (await fs.readFile(sqlProjPath)).toString(); + newSqlProjContents.should.equal(originalSqlProjContents, 'SSDT .sqlproj contents should not have changed when update prompt is rejected') sinon.restore(); }); it('Should not show warning message for non-SSDT projects that have the additional information for Build', async function (): Promise { // setup test files - const folderPath = await testUtils.generateTestFolderPath(); - const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline, folderPath); - await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, folderPath); + const folderPath = await testUtils.generateTestFolderPath(this.test); + const sqlProjPath = await testUtils.createTestSqlProjFile(this.test, baselines.openProjectFileBaseline, folderPath); + await testUtils.createTestDataSources(this.test, baselines.openDataSourcesBaseline, folderPath); - const project = await Project.openProject(Uri.file(sqlProjPath).fsPath); // no error thrown - - should(project.importedTargets.length).equal(3); // additional target should exist by default + await Project.openProject(Uri.file(sqlProjPath).fsPath); // no error thrown }); it('Should not show update project warning message when opening sdk style project using Sdk node', async function (): Promise { - await shouldNotShowUpdateWarning(baselines.newSdkStyleProjectSdkNodeBaseline); + await shouldNotShowUpdateWarning(this.test, baselines.newSdkStyleProjectSdkNodeBaseline); }); it('Should not show update project warning message when opening sdk style project using Project node with Sdk attribute', async function (): Promise { - await shouldNotShowUpdateWarning(baselines.newSdkStyleProjectSdkProjectAttributeBaseline); + await shouldNotShowUpdateWarning(this.test, baselines.newSdkStyleProjectSdkProjectAttributeBaseline); }); it('Should not show update project warning message when opening sdk style project using Import node with Sdk attribute', async function (): Promise { - await shouldNotShowUpdateWarning(baselines.newStyleProjectSdkImportAttributeBaseline); + await shouldNotShowUpdateWarning(this.test, baselines.newStyleProjectSdkImportAttributeBaseline); }); - async function shouldNotShowUpdateWarning(baselineFile: string): Promise { + async function shouldNotShowUpdateWarning(test: Mocha.Runnable | undefined, baselineFile: string): Promise { // setup test files - const folderPath = await testUtils.generateTestFolderPath(); - const sqlProjPath = await testUtils.createTestSqlProjFile(baselineFile, folderPath); + const folderPath = await testUtils.generateTestFolderPath(test); + const sqlProjPath = await testUtils.createTestSqlProjFile(test, baselineFile, folderPath); const spy = sinon.spy(window, 'showWarningMessage'); const project = await Project.openProject(Uri.file(sqlProjPath).fsPath); - should(spy.notCalled).be.true(); - should(project.isSdkStyleProject).be.true(); + (project.isCrossPlatformCompatible).should.be.true('Project should be detected as cross-plat compatible'); + (spy.notCalled).should.be.true('Prompt to update .sqlproj should not have been shown for cross-plat project.'); } }); -async function testUpdateInRoundTrip(fileBeforeupdate: string, fileAfterUpdate: string): Promise { - const stub = sinon.stub(window, 'showWarningMessage').returns(Promise.resolve(constants.yesString)); - - projFilePath = await testUtils.createTestSqlProjFile(fileBeforeupdate); +async function testUpdateInRoundTrip(test: Mocha.Runnable | undefined, fileBeforeupdate: string): Promise { + const projFilePath = await testUtils.createTestSqlProjFile(test, fileBeforeupdate); const project = await Project.openProject(projFilePath); // project gets updated if needed in openProject() + should(project.isCrossPlatformCompatible).be.false('Project should not be cross-plat compatible before conversion'); - should(await exists(projFilePath + '_backup')).equal(true, 'Backup file should have been generated before the project was updated'); - should(project.importedTargets.length).equal(3); // additional target added by updateProjectForRoundTrip method + project.isCrossPlatformCompatible.should.be.false('Project should not be cross-plat compatible before conversion'); - let projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText).equal(fileAfterUpdate.trim()); + await project.updateProjectForRoundTrip(); + + (project.isCrossPlatformCompatible).should.be.true('Project should be cross-plat compatible after conversion'); + (await exists(projFilePath + '_backup')).should.be.true('Backup file should have been generated before the project was updated'); - should(stub.calledOnce).be.true('showWarningMessage should have been called exactly once'); sinon.restore(); } - -describe('Project: legacy to SDK-style updates', function (): void { - before(async function (): Promise { - await baselines.loadBaselines(); - }); - - beforeEach(function (): void { - sinon.restore(); - }); - - after(async function (): Promise { - await testUtils.deleteGeneratedTestFolder(); - }); - - it('Should update legacy style project to SDK-style', async function (): Promise { - const projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const list: Uri[] = []; - await testUtils.createDummyFileStructure(true, list, path.dirname(projFilePath)); - const project = await Project.openProject(projFilePath); - await project.addToProject(list); - - const beforeFileCount = project.files.filter(f => f.type === EntryType.File).length; - const beforeFolderCount = project.files.filter(f => f.type === EntryType.Folder).length; - should(beforeFolderCount).equal(2, 'There should be 2 folders in the project'); - should(beforeFileCount).equal(11, 'There should be 11 files in the project'); - should(project.importedTargets.length).equal(3, 'SSDT and ADS imports should be in the project'); - should(project.isSdkStyleProject).equal(false); - let projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText.includes('')).equal(true, 'sqlproj should have VisualStudioVersion property with empty condition before converting'); - should(projFileText.includes('')).equal(true, 'sqlproj should have SSDTExists property before converting'); - should(projFileText.includes('')).equal(true, 'sqlproj should have VisualStudioVersion property with SSDTExists condition before converting'); - - await project.convertProjectToSdkStyle(); - - should(await exists(projFilePath + '_backup')).equal(true, 'Backup file should have been generated before the project was updated'); - should(project.importedTargets.length).equal(0, 'SSDT and ADS imports should have been removed'); - should(project.isSdkStyleProject).equal(true); - - projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText.includes(' f.type === EntryType.File).length).equal(beforeFileCount, 'Same number of files should be included after Build Includes are removed'); - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(beforeFolderCount, 'Same number of folders should be included after Folder Includes are removed'); - should(projFileText.includes('')).equal(false, 'VisualStudioVersion property with empty condition should be removed'); - should(projFileText.includes('')).equal(false, 'SSDTExists property should be removed'); - should(projFileText.includes('')).equal(false, 'VisualStudioVersion property with SSDTExists condition should be removed'); - }); - - it('Should not fail if legacy style project does not have Properties folder in sqlproj', async function (): Promise { - const projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileNoPropertiesFolderBaseline); - const list: Uri[] = []; - await testUtils.createDummyFileStructure(true, list, path.dirname(projFilePath)); - const project = await Project.openProject(projFilePath); - await project.addToProject(list); - - const beforeFolderCount = project.files.filter(f => f.type === EntryType.Folder).length; - - await project.convertProjectToSdkStyle(); - - should(project.isSdkStyleProject).equal(true); - const projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText.includes(' f.type === EntryType.Folder).length).equal(beforeFolderCount, 'Same number of folders should be included after Folder Includes are removed'); - }); - - it('Should exclude sql files that were not in previously included in legacy style sqlproj', async function (): Promise { - const projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const list: Uri[] = []; - await testUtils.createDummyFileStructure(true, list, path.dirname(projFilePath)); - const project = await Project.openProject(projFilePath); - - // don't add file1.sql, folder1\file1.sql and folder2\file1.sql - await project.addToProject(list.filter(f => !f.fsPath.includes('file1.sql'))); - - const beforeFileCount = project.files.filter(f => f.type === EntryType.File).length; - const beforeFolderCount = project.files.filter(f => f.type === EntryType.Folder).length; - should(beforeFileCount).equal(8, 'There should be 8 files in the project before converting'); - - await project.convertProjectToSdkStyle(); - - should(project.isSdkStyleProject).equal(true); - const projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText.includes(' f.type === EntryType.File).length).equal(beforeFileCount, 'Same number of files should be included after Build Includes are removed'); - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(beforeFolderCount, 'Same number of folders should be included after Folder Includes are removed'); - }); - - it('Should keep Build Includes for files outside of project folder', async function (): Promise { - const testFolderPath = await testUtils.generateTestFolderPath(); - const mainProjectPath = path.join(testFolderPath, 'project'); - const otherFolderPath = path.join(testFolderPath, 'other'); - projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline, mainProjectPath); - let list: Uri[] = []; - await testUtils.createDummyFileStructure(true, list, path.dirname(projFilePath)); - - // create files outside of project folder that are included in the project file - await fs.mkdir(otherFolderPath); - const otherFiles = await testUtils.createOtherDummyFiles(otherFolderPath); - list = list.concat(otherFiles); - - const project = await Project.openProject(projFilePath); - - // add all the files, except the pre and post deploy scripts - await project.addToProject(list.filter(f => !f.fsPath.includes('Script.'))); - - const beforeFileCount = project.files.filter(f => f.type === EntryType.File).length; - const beforeFolderCount = project.files.filter(f => f.type === EntryType.Folder).length; - should(beforeFileCount).equal(19, 'There should be 19 files in the project'); - - await project.convertProjectToSdkStyle(); - - should(project.isSdkStyleProject).equal(true); - const projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText.match(/ f.type === EntryType.File).length).equal(beforeFileCount, 'Same number of files should be included after Build Includes are removed'); - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(beforeFolderCount, 'Same number of folders should be included after Folder Includes are removed'); - }); - - it('Should list previously included empty folders in sqlproj after converting to SDK-style', async function (): Promise { - const projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const list: Uri[] = []; - const folderPath = path.dirname(projFilePath); - await testUtils.createDummyFileStructure(true, list, folderPath); - const project = await Project.openProject(projFilePath); - await project.addToProject(list); - - await project.addFolderItem('folder3'); - await project.addFolderItem('folder3\\nestedFolder'); - await project.addFolderItem('folder4'); - - const beforeFolderCount = project.files.filter(f => f.type === EntryType.Folder).length; - should(beforeFolderCount).equal(5); - - await project.convertProjectToSdkStyle(); - - should(project.isSdkStyleProject).equal(true); - const projFileText = (await fs.readFile(projFilePath)).toString(); - should(projFileText.includes('')).equal(false, 'There should not be a folder include for folder3\\nestedFolder because it gets included by the nestedFolder entry'); - should(projFileText.includes('')).equal(true, 'There should be a folder include for folder3\\nestedFolder'); - should(projFileText.includes('')).equal(true, 'There should be a folder include for folder4'); - should(project.files.filter(f => f.type === EntryType.Folder).length).equal(beforeFolderCount, 'Same number of folders should be included after Folder Includes are removed'); - }); - - it('Should rollback changes if there was an error during conversion to SDK-style', async function (): Promise { - const folderPath = await testUtils.generateTestFolderPath(); - const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline, folderPath); - const project = await Project.openProject(Uri.file(sqlProjPath).fsPath); - should(project.isSdkStyleProject).equal(false); - - // add an empty folder so that addFolderItem() will get called during the conversion. Empty folders aren't included by glob, so they need to be added to the sqlproj - // to show up in the project tree - await project.addFolderItem('folder1'); - - sinon.stub(Project.prototype, 'addFolderItem').throwsException('error'); - const result = await project.convertProjectToSdkStyle(); - - should(result).equal(false); - should(project.isSdkStyleProject).equal(false); - should(project.importedTargets.length).equal(3, 'SSDT and ADS imports should still be there'); - }); - - it('Should not update project and no backup file should be created when project is already SDK-style', async function (): Promise { - // setup test files - const folderPath = await testUtils.generateTestFolderPath(); - const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectBaseline, folderPath); - - const project = await Project.openProject(Uri.file(sqlProjPath).fsPath); - should(project.isSdkStyleProject).equal(true); - await project.convertProjectToSdkStyle(); - - should(await exists(sqlProjPath + '_backup')).equal(false, 'No backup file should have been created'); - should(project.isSdkStyleProject).equal(true); - }); - - it('Should not update project and no backup file should be created when it is an SSDT project that has not been updated to work in ADS', async function (): Promise { - sinon.stub(window, 'showWarningMessage').returns(Promise.resolve(constants.noString)); - // setup test files - const folderPath = await testUtils.generateTestFolderPath(); - const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.SSDTProjectFileBaseline, folderPath); - - const project = await Project.openProject(Uri.file(sqlProjPath).fsPath); - should(project.isSdkStyleProject).equal(false); - should(project.importedTargets.length).equal(2, 'Project should have 2 SSDT imports'); - await project.convertProjectToSdkStyle(); - - should(await exists(sqlProjPath + '_backup')).equal(false, 'No backup file should have been created'); - should(project.importedTargets.length).equal(2, 'Project imports should not have been changed'); - should(project.isSdkStyleProject).equal(false); - }); -}); diff --git a/extensions/sql-database-projects/src/test/projectController.test.ts b/extensions/sql-database-projects/src/test/projectController.test.ts index d84edacbeb..bda997d08f 100644 --- a/extensions/sql-database-projects/src/test/projectController.test.ts +++ b/extensions/sql-database-projects/src/test/projectController.test.ts @@ -20,7 +20,7 @@ import { SqlDatabaseProjectTreeViewProvider } from '../controllers/databaseProje import { ProjectsController } from '../controllers/projectController'; import { promises as fs } from 'fs'; import { createContext, TestContext, mockDacFxResult, mockConnectionProfile } from './testContext'; -import { Project, reservedProjectFolders } from '../models/project'; +import { Project } from '../models/project'; import { PublishDatabaseDialog } from '../dialogs/publishDatabaseDialog'; import { ProjectRootTreeItem } from '../models/tree/projectTreeItem'; import { FolderNode, FileNode } from '../models/tree/fileFolderTreeItem'; @@ -30,7 +30,8 @@ import { IDacpacReferenceSettings } from '../models/IDatabaseReferenceSettings'; import { CreateProjectFromDatabaseDialog } from '../dialogs/createProjectFromDatabaseDialog'; import { ImportDataModel } from '../models/api/import'; import { EntryType, ItemType, SqlTargetPlatform } from 'sqldbproj'; -import { SystemDatabaseReferenceProjectEntry, SystemDatabase, FileProjectEntry } from '../models/projectEntry'; +import { FileProjectEntry } from '../models/projectEntry'; +import { SystemDatabase } from 'mssql'; let testContext: TestContext; @@ -56,7 +57,7 @@ describe('ProjectsController', function (): void { describe('Project file operations and prompting', function (): void { it('Should create new sqlproj file with correct specified target platform', async function (): Promise { const projController = new ProjectsController(testContext.outputChannel); - const projFileDir = path.join(testUtils.generateBaseFolderName(), `TestProject_${new Date().getTime()}`); + const projFileDir = await testUtils.generateTestFolderPath(this.test); const projTargetPlatform = SqlTargetPlatform.sqlAzure; // default is SQL Server 2022 const projFilePath = await projController.createNewProject({ @@ -75,7 +76,7 @@ describe('ProjectsController', function (): void { it('Should create new edge project with expected template files', async function (): Promise { const projController = new ProjectsController(testContext.outputChannel); - const projFileDir = path.join(testUtils.generateBaseFolderName(), `TestProject_${new Date().getTime()}`); + const projFileDir = await testUtils.generateTestFolderPath(this.test); const projFilePath = await projController.createNewProject({ newProjName: 'TestProjectName', @@ -111,7 +112,7 @@ describe('ProjectsController', function (): void { sinon.stub(utils, 'sanitizeStringForFilename').returns(tableName); const spy = sinon.spy(vscode.window, 'showErrorMessage'); const projController = new ProjectsController(testContext.outputChannel); - let project = await testUtils.createTestProject(baselines.newProjectFileBaseline); + let project = await testUtils.createTestProject(this.test, baselines.newProjectFileBaseline); should(project.files.length).equal(0, 'There should be no files'); await projController.addItemPrompt(project, '', { itemType: ItemType.script }); @@ -127,7 +128,7 @@ describe('ProjectsController', function (): void { sinon.stub(vscode.window, 'showQuickPick').resolves(undefined); const spy = sinon.spy(vscode.window, 'showErrorMessage'); const projController = new ProjectsController(testContext.outputChannel); - const project = await testUtils.createTestProject(baselines.newProjectFileBaseline); + const project = await testUtils.createTestProject(this.test, baselines.newProjectFileBaseline); should(project.files.length).equal(0, 'There should be no files'); await projController.addItemPrompt(project, ''); @@ -141,7 +142,7 @@ describe('ProjectsController', function (): void { sinon.stub(utils, 'sanitizeStringForFilename').returns(tableName); const spy = sinon.spy(vscode.window, 'showErrorMessage'); const projController = new ProjectsController(testContext.outputChannel); - let project = await testUtils.createTestProject(baselines.newProjectFileBaseline); + let project = await testUtils.createTestProject(this.test, baselines.newProjectFileBaseline); should(project.files.length).equal(0, 'There should be no files'); await projController.addItemPrompt(project, '', { itemType: ItemType.script }); @@ -171,22 +172,22 @@ describe('ProjectsController', function (): void { sinon.stub(utils, 'sanitizeStringForFilename').returns(folderName); const projController = new ProjectsController(testContext.outputChannel); - let project = await testUtils.createTestProject(baselines.newProjectFileBaseline); + let project = await testUtils.createTestProject(this.test, baselines.newProjectFileBaseline); const projectRoot = new ProjectRootTreeItem(project); - should(project.files.length).equal(0, 'There should be no other folders'); + should(project.folders.length).equal(0, 'There should be no other folders'); await projController.addFolderPrompt(createWorkspaceTreeItem(projectRoot)); // reload project project = await Project.openProject(project.projectFilePath); - should(project.files.length).equal(1, 'Folder should be successfully added'); + should(project.folders.length).equal(1, 'Folder should be successfully added'); stub.restore(); await verifyFolderNotAdded(folderName, projController, project, projectRoot); // reserved folder names - for (let i in reservedProjectFolders) { - await verifyFolderNotAdded(reservedProjectFolders[i], projController, project, projectRoot); + for (let i in constants.reservedProjectFolders) { + await verifyFolderNotAdded(constants.reservedProjectFolders[i], projController, project, projectRoot); } }); @@ -196,47 +197,48 @@ describe('ProjectsController', function (): void { sinon.stub(utils, 'sanitizeStringForFilename').returns(folderName); const projController = new ProjectsController(testContext.outputChannel); - let project = await testUtils.createTestProject(baselines.openProjectFileBaseline); + let project = await testUtils.createTestProject(this.test, baselines.openProjectFileBaseline); const projectRoot = new ProjectRootTreeItem(project); // make sure it's ok to add these folders if they aren't where the reserved folders are at the root of the project let node = projectRoot.children.find(c => c.friendlyName === 'Tables'); sinon.restore(); - for (let i in reservedProjectFolders) { + for (let i in constants.reservedProjectFolders) { // reload project project = await Project.openProject(project.projectFilePath); - await verifyFolderAdded(reservedProjectFolders[i], projController, project, node); + await verifyFolderAdded(constants.reservedProjectFolders[i], projController, project, node); } }); async function verifyFolderAdded(folderName: string, projController: ProjectsController, project: Project, node: BaseProjectTreeItem): Promise { - const beforeFileCount = project.files.length; - let beforeFiles = project.files.map(f => f.relativePath); + const beforeFolderCount = project.folders.length; + let beforeFolders = project.folders.map(f => f.relativePath); sinon.stub(vscode.window, 'showInputBox').resolves(folderName); sinon.stub(utils, 'sanitizeStringForFilename').returns(folderName); await projController.addFolderPrompt(createWorkspaceTreeItem(node)); // reload project project = await Project.openProject(project.projectFilePath); - should(project.files.length).equal(beforeFileCount + 1, `File count should be increased by one after adding the folder ${folderName}. before files: ${JSON.stringify(beforeFiles)}/n after files: ${JSON.stringify(project.files.map(f => f.relativePath))}`); + should(project.folders.length).equal(beforeFolderCount + 1, `Folder count should be increased by one after adding the folder ${folderName}. before folders: ${JSON.stringify(beforeFolders)}/n after folders: ${JSON.stringify(project.files.map(f => f.relativePath))}`); sinon.restore(); } async function verifyFolderNotAdded(folderName: string, projController: ProjectsController, project: Project, node: BaseProjectTreeItem): Promise { - const beforeFileCount = project.files.length; + const beforeFileCount = project.folders.length; const showInputBoxStub = sinon.stub(vscode.window, 'showInputBox').resolves(folderName); const showErrorMessageSpy = sinon.spy(vscode.window, 'showErrorMessage'); await projController.addFolderPrompt(createWorkspaceTreeItem(node)); should(showErrorMessageSpy.calledOnce).be.true('showErrorMessage should have been called exactly once'); const msg = constants.folderAlreadyExists(folderName); should(showErrorMessageSpy.calledWith(msg)).be.true(`showErrorMessage not called with expected message '${msg}' Actual '${showErrorMessageSpy.getCall(0).args[0]}'`); - should(project.files.length).equal(beforeFileCount, 'File count should be the same as before the folder was attempted to be added'); + should(project.folders.length).equal(beforeFileCount, 'File count should be the same as before the folder was attempted to be added'); showInputBoxStub.restore(); showErrorMessageSpy.restore(); } - it('Should delete nested ProjectEntry from node', async function (): Promise { - let proj = await testUtils.createTestProject(templates.newSqlProjectTemplate); + // TODO: move test to DacFx and fix delete + it.skip('Should delete nested ProjectEntry from node', async function (): Promise { + let proj = await testUtils.createTestProject(this.test, templates.newSqlProjectTemplate); const setupResult = await setupDeleteExcludeTest(proj); const scriptEntry = setupResult[0], projTreeRoot = setupResult[1], preDeployEntry = setupResult[2], postDeployEntry = setupResult[3], noneEntry = setupResult[4]; @@ -252,11 +254,11 @@ describe('ProjectsController', function (): void { proj = await Project.openProject(proj.projectFilePath); // reload edited sqlproj from disk // confirm result - should(proj.files.length).equal(1, 'number of file/folder entries'); // lowerEntry and the contained scripts should be deleted - should(proj.files[0].relativePath).equal('UpperFolder\\'); - should(proj.preDeployScripts.length).equal(0); - should(proj.postDeployScripts.length).equal(0); - should(proj.noneDeployScripts.length).equal(0); + should(proj.files.length).equal(3, 'number of file entries'); // lowerEntry and the contained scripts should be deleted + should(proj.folders[0].relativePath).equal('UpperFolder'); + should(proj.preDeployScripts.length).equal(0, 'Pre Deployment scripts should have been deleted'); + should(proj.postDeployScripts.length).equal(0, 'Post Deployment scripts should have been deleted'); + should(proj.noneDeployScripts.length).equal(0, 'None file should have been deleted'); should(await utils.exists(scriptEntry.fsUri.fsPath)).equal(false, 'script is supposed to be deleted'); should(await utils.exists(preDeployEntry.fsUri.fsPath)).equal(false, 'pre-deployment script is supposed to be deleted'); @@ -266,7 +268,7 @@ describe('ProjectsController', function (): void { it('Should delete database references', async function (): Promise { // setup - openProject baseline has a system db reference to master - let proj = await testUtils.createTestProject(baselines.openProjectFileBaseline); + let proj = await testUtils.createTestProject(this.test, baselines.openProjectFileBaseline); const projController = new ProjectsController(testContext.outputChannel); sinon.stub(vscode.window, 'showWarningMessage').returns(Promise.resolve(constants.yesString)); @@ -277,6 +279,7 @@ describe('ProjectsController', function (): void { databaseVariable: 'test2Db', suppressMissingDependenciesErrors: false }); + // add project reference await proj.addProjectReference({ projectName: 'project1', @@ -286,8 +289,6 @@ describe('ProjectsController', function (): void { }); const projTreeRoot = new ProjectRootTreeItem(proj); - // reload project - proj = await Project.openProject(proj.projectFilePath); should(proj.databaseReferences.length).equal(3, 'Should start with 3 database references'); const databaseReferenceNodeChildren = projTreeRoot.children.find(x => x.friendlyName === constants.databaseReferencesNodeName)?.children; @@ -302,7 +303,7 @@ describe('ProjectsController', function (): void { }); it('Should exclude nested ProjectEntry from node', async function (): Promise { - let proj = await testUtils.createTestProject(templates.newSqlProjectTemplate); + let proj = await testUtils.createTestProject(this.test, templates.newSqlProjectTemplate); const setupResult = await setupDeleteExcludeTest(proj); const scriptEntry = setupResult[0], projTreeRoot = setupResult[1], preDeployEntry = setupResult[2], postDeployEntry = setupResult[3], noneEntry = setupResult[4]; @@ -317,11 +318,11 @@ describe('ProjectsController', function (): void { proj = await Project.openProject(proj.projectFilePath); // reload edited sqlproj from disk // confirm result - should(proj.files.length).equal(1, 'number of file/folder entries'); // LowerFolder and the contained scripts should be deleted - should(proj.files[0].relativePath).equal('UpperFolder\\'); // UpperFolder should still be there - should(proj.preDeployScripts.length).equal(0); - should(proj.postDeployScripts.length).equal(0); - should(proj.noneDeployScripts.length).equal(0); + should(proj.files.length).equal(2, 'number of file entries'); // LowerFolder and the contained scripts should be deleted + should(proj.folders.find(f => f.relativePath === 'UpperFolder')).not.equal(undefined, 'UpperFolder should still be there'); + should(proj.preDeployScripts.length).equal(0, 'Pre deployment scripts'); + should(proj.postDeployScripts.length).equal(0, 'Post deployment scripts'); + should(proj.noneDeployScripts.length).equal(0, 'None files'); should(await utils.exists(scriptEntry.fsUri.fsPath)).equal(true, 'script is supposed to still exist on disk'); should(await utils.exists(preDeployEntry.fsUri.fsPath)).equal(true, 'pre-deployment script is supposed to still exist on disk'); @@ -329,8 +330,9 @@ describe('ProjectsController', function (): void { should(await utils.exists(noneEntry.fsUri.fsPath)).equal(true, 'none entry pre-deployment script is supposed to still exist on disk'); }); - it('Should delete folders with excluded items', async function (): Promise { - let proj = await testUtils.createTestProject(templates.newSqlProjectTemplate); + // TODO: move test to DacFx and fix delete + it.skip('Should delete folders with excluded items', async function (): Promise { + let proj = await testUtils.createTestProject(this.test, templates.newSqlProjectTemplate); const setupResult = await setupDeleteExcludeTest(proj); const scriptEntry = setupResult[0], projTreeRoot = setupResult[1]; @@ -358,8 +360,8 @@ describe('ProjectsController', function (): void { it('Should reload correctly after changing sqlproj file', async function (): Promise { // create project - const folderPath = await testUtils.generateTestFolderPath(); - const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline, folderPath); + const folderPath = await testUtils.generateTestFolderPath(this.test); + const sqlProjPath = await testUtils.createTestSqlProjFile(this.test, baselines.newProjectFileBaseline, folderPath); const treeProvider = new SqlDatabaseProjectTreeViewProvider(); const projController = new ProjectsController(testContext.outputChannel); let project = await Project.openProject(vscode.Uri.file(sqlProjPath).fsPath); @@ -375,7 +377,7 @@ describe('ProjectsController', function (): void { // calling this because this gets called in the projectProvider.getProjectTreeDataProvider(), which is called by workspaceTreeDataProvider // when notifyTreeDataChanged() happens // reload project - project = await Project.openProject(sqlProjPath); + project = await Project.openProject(sqlProjPath, false, true); treeProvider.load([project]); // check that the new project is in the tree @@ -388,7 +390,7 @@ describe('ProjectsController', function (): void { const postDeployScriptName = 'PostDeployScript1.sql'; const projController = new ProjectsController(testContext.outputChannel); - const project = await testUtils.createTestProject(baselines.newProjectFileBaseline); + const project = await testUtils.createTestProject(this.test, baselines.newProjectFileBaseline); sinon.stub(vscode.window, 'showInputBox').resolves(preDeployScriptName); sinon.stub(utils, 'sanitizeStringForFilename').returns(preDeployScriptName); @@ -408,18 +410,13 @@ describe('ProjectsController', function (): void { sinon.stub(vscode.window, 'showQuickPick').resolves({ label: SqlTargetPlatform.sqlAzure }); const projController = new ProjectsController(testContext.outputChannel); - const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline); + const sqlProjPath = await testUtils.createTestSqlProjFile(this.test, baselines.openProjectFileBaseline); const project = await Project.openProject(sqlProjPath); should(project.getProjectTargetVersion()).equal(constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlServer2019)); should(project.databaseReferences.length).equal(1, 'Project should have one database reference to master'); - should(project.databaseReferences[0].fsUri.fsPath).containEql(constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlServer2019)); - should((project.databaseReferences[0]).ssdtUri.fsPath).containEql(constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlServer2019)); await projController.changeTargetPlatform(project); should(project.getProjectTargetVersion()).equal(constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlAzure)); - // verify system db reference got updated too - should(project.databaseReferences[0].fsUri.fsPath).containEql(constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlAzure)); - should((project.databaseReferences[0]).ssdtUri.fsPath).containEql(constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlAzure)); }); }); @@ -440,9 +437,9 @@ describe('ProjectsController', function (): void { }); it('Callbacks are hooked up and called from Publish dialog', async function (): Promise { - const projectFile = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline) + const projectFile = await testUtils.createTestSqlProjFile(this.test, baselines.openProjectFileBaseline) const projFolder = path.dirname(projectFile); - await testUtils.createTestDataSources(baselines.openDataSourcesBaseline, projFolder); + await testUtils.createTestDataSources(this.test, baselines.openDataSourcesBaseline, projFolder); const proj = await Project.openProject(projectFile); const publishHoller = 'hello from callback for publish()'; @@ -501,12 +498,12 @@ describe('ProjectsController', function (): void { projController.callBase = true; projController.setup(x => x.buildProject(TypeMoq.It.isAny())).returns(async () => { - builtDacpacPath = await testUtils.createTestFile(fakeDacpacContents, 'output.dacpac'); + builtDacpacPath = await testUtils.createTestFile(this.test, fakeDacpacContents, 'output.dacpac'); return builtDacpacPath; }); sinon.stub(utils, 'getDacFxService').resolves(testContext.dacFxService.object); - const proj = await testUtils.createTestProject(baselines.openProjectFileBaseline); + const proj = await testUtils.createTestProject(this.test, baselines.openProjectFileBaseline); await projController.object.publishOrScriptProject(proj, { connectionUri: '', databaseName: '', serverName: '' }, false); @@ -525,23 +522,26 @@ describe('ProjectsController', function (): void { }); it('Should create list of all files and folders correctly', async function (): Promise { - const testFolderPath = await testUtils.createDummyFileStructure(); + // dummy structure is 2 files (one .sql, one .txt) under parent folder + 2 directories with 5 .sql scripts each + const testFolderPath = await testUtils.createDummyFileStructure(this.test); const projController = new ProjectsController(testContext.outputChannel); - const fileList = await projController.generateList(testFolderPath); + const fileList = await projController.generateScriptList(testFolderPath); - should(fileList.length).equal(15); // Parent folder + 2 files under parent folder + 2 directories with 5 files each + // script list should only include the .sql files, no folders and not the .txt file + (fileList.length).should.equal(11, 'number of files returned by generateScriptList()'); + (fileList.filter(x => path.extname(x.fsPath) !== constants.sqlFileExtension).length).should.equal(0, 'number of non-.sql files'); }); it('Should error out for inaccessible path', async function (): Promise { const spy = sinon.spy(vscode.window, 'showErrorMessage'); - let testFolderPath = await testUtils.generateTestFolderPath(); + let testFolderPath = await testUtils.generateTestFolderPath(this.test); testFolderPath += '_nonexistentFolder'; // Modify folder path to point to a nonexistent location const projController = new ProjectsController(testContext.outputChannel); - await projController.generateList(testFolderPath); + await projController.generateScriptList(testFolderPath); should(spy.calledOnce).be.true('showErrorMessage should have been called'); const msg = constants.cannotResolvePath(testFolderPath); should(spy.calledWith(msg)).be.true(`showErrorMessage not called with expected message '${msg}' Actual '${spy.getCall(0).args[0]}'`); @@ -597,7 +597,7 @@ describe('ProjectsController', function (): void { }); it('Should set model filePath correctly for ExtractType = File', async function (): Promise { - let folderPath = await testUtils.generateTestFolderPath(); + let folderPath = await testUtils.generateTestFolderPath(this.test); let projectName = 'My Project'; let importPath; let model: ImportDataModel = { connectionUri: 'My Id', database: 'My Database', projName: projectName, filePath: folderPath, version: '1.0.0.0', extractTarget: mssql.ExtractTarget['file'], sdkStyle: false }; @@ -610,7 +610,7 @@ describe('ProjectsController', function (): void { }); it('Should set model filePath correctly for ExtractType = Schema/Object Type', async function (): Promise { - let folderPath = await testUtils.generateTestFolderPath(); + let folderPath = await testUtils.generateTestFolderPath(this.test); let projectName = 'My Project'; let importPath; let model: ImportDataModel = { connectionUri: 'My Id', database: 'My Database', projName: projectName, filePath: folderPath, version: '1.0.0.0', extractTarget: mssql.ExtractTarget['schemaObjectType'], sdkStyle: false }; @@ -639,7 +639,7 @@ describe('ProjectsController', function (): void { }); it('Callbacks are hooked up and called from Add database reference dialog', async function (): Promise { - const projPath = path.dirname(await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline)); + const projPath = path.dirname(await testUtils.createTestSqlProjFile(this.test, baselines.openProjectFileBaseline)); const proj = new Project(projPath); const addDbRefHoller = 'hello from callback for addDatabaseReference()'; @@ -650,7 +650,7 @@ describe('ProjectsController', function (): void { addDbReferenceDialog.callBase = true; addDbReferenceDialog.setup(x => x.addReferenceClick()).returns(() => { return projController.object.addDatabaseReferenceCallback(proj, - { systemDb: SystemDatabase.master, databaseName: 'master', suppressMissingDependenciesErrors: false }, + { systemDb: SystemDatabase.Master, databaseName: 'master', suppressMissingDependenciesErrors: false }, { treeDataProvider: new SqlDatabaseProjectTreeViewProvider(), element: undefined }); }); addDbReferenceDialog.setup(x => x.openDialog()).returns(() => Promise.resolve()); @@ -669,9 +669,9 @@ describe('ProjectsController', function (): void { should(holler).equal(addDbRefHoller, 'executionCallback() is supposed to have been setup and called for add database reference scenario'); }); - it('Should not allow adding circular project references', async function (): Promise { - const projPath1 = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline); - const projPath2 = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); + it.skip('Should not allow adding circular project references', async function (): Promise { + const projPath1 = await testUtils.createTestSqlProjFile(this.test, baselines.openProjectFileBaseline); + const projPath2 = await testUtils.createTestSqlProjFile(this.test, baselines.newProjectFileBaseline); const projController = new ProjectsController(testContext.outputChannel); const project1 = await Project.openProject(vscode.Uri.file(projPath1).fsPath); @@ -707,8 +707,8 @@ describe('ProjectsController', function (): void { should(showErrorMessageSpy.called).be.true('showErrorMessage should have been called'); }); - it('Should add dacpac references as relative paths', async function (): Promise { - const projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); + it.skip('Should add dacpac references as relative paths', async function (): Promise { + const projFilePath = await testUtils.createTestSqlProjFile(this.test, baselines.newProjectFileBaseline); const projController = new ProjectsController(testContext.outputChannel); const project1 = await Project.openProject(vscode.Uri.file(projFilePath).fsPath); @@ -731,7 +731,7 @@ describe('ProjectsController', function (): void { { treeDataProvider: new SqlDatabaseProjectTreeViewProvider(), element: undefined }); should(showErrorMessageSpy.notCalled).be.true('showErrorMessage should not have been called'); should(project1.databaseReferences.length).equal(1, 'Dacpac reference should have been added'); - should(project1.databaseReferences[0].databaseName).equal('sameFolderTest'); + should(project1.databaseReferences[0].referenceName).equal('sameFolderTest'); should(project1.databaseReferences[0].pathForSqlProj()).equal('sameFolderTest.dacpac'); // make sure reference to sameFolderTest.dacpac was added to project file let projFileText = (await fs.readFile(projFilePath)).toString(); @@ -746,7 +746,7 @@ describe('ProjectsController', function (): void { { treeDataProvider: new SqlDatabaseProjectTreeViewProvider(), element: undefined }); should(showErrorMessageSpy.notCalled).be.true('showErrorMessage should not have been called'); should(project1.databaseReferences.length).equal(2, 'Another dacpac reference should have been added'); - should(project1.databaseReferences[1].databaseName).equal('nestedFolderTest'); + should(project1.databaseReferences[1].referenceName).equal('nestedFolderTest'); should(project1.databaseReferences[1].pathForSqlProj()).equal('refs\\nestedFolderTest.dacpac'); // make sure reference to nestedFolderTest.dacpac was added to project file projFileText = (await fs.readFile(projFilePath)).toString(); @@ -761,7 +761,7 @@ describe('ProjectsController', function (): void { { treeDataProvider: new SqlDatabaseProjectTreeViewProvider(), element: undefined }); should(showErrorMessageSpy.notCalled).be.true('showErrorMessage should not have been called'); should(project1.databaseReferences.length).equal(3, 'Another dacpac reference should have been added'); - should(project1.databaseReferences[2].databaseName).equal('outsideFolderTest'); + should(project1.databaseReferences[2].referenceName).equal('outsideFolderTest'); should(project1.databaseReferences[2].pathForSqlProj()).equal('..\\someFolder\\outsideFolderTest.dacpac'); // make sure reference to outsideFolderTest.dacpac was added to project file projFileText = (await fs.readFile(projFilePath)).toString(); @@ -770,9 +770,10 @@ describe('ProjectsController', function (): void { }); describe('AutoRest generation', function (): void { - it('Should create project from autorest-generated files', async function (): Promise { - const parentFolder = await testUtils.generateTestFolderPath(); - await testUtils.createDummyFileStructure(); + // skipping for now because this feature is hidden under preview flag + it.skip('Should create project from autorest-generated files', async function (): Promise { + const parentFolder = await testUtils.generateTestFolderPath(this.test); + await testUtils.createDummyFileStructure(this.test); const specName = 'DummySpec.yaml'; const renamedProjectName = 'RenamedProject'; const newProjFolder = path.join(parentFolder, renamedProjectName); @@ -793,8 +794,8 @@ describe('ProjectsController', function (): void { }); projController.setup(x => x.generateAutorestFiles(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(async () => { - await testUtils.createDummyFileStructure(true, fileList, newProjFolder); - await testUtils.createTestFile('SELECT \'This is a post-deployment script\'', constants.autorestPostDeploymentScriptName, newProjFolder); + await testUtils.createDummyFileStructure(this.test, true, fileList, newProjFolder); + await testUtils.createTestFile(this.test, 'SELECT \'This is a post-deployment script\'', constants.autorestPostDeploymentScriptName, newProjFolder); return 'some dummy console output'; }); @@ -823,7 +824,7 @@ describe('ProjectsController', function (): void { const spy = sinon.spy(vscode.window, 'showErrorMessage'); sinon.stub(vscode.window, 'showWarningMessage').returns(Promise.resolve(constants.move)); - let proj = await testUtils.createTestProject(baselines.openSdkStyleSqlProjectBaseline); + let proj = await testUtils.createTestProject(this.test, baselines.openSdkStyleSqlProjectBaseline); const projTreeRoot = await setupMoveTest(proj); @@ -846,7 +847,7 @@ describe('ProjectsController', function (): void { const spy = sinon.spy(vscode.window, 'showErrorMessage'); sinon.stub(vscode.window, 'showWarningMessage').returns(Promise.resolve(constants.move)); - let proj = await testUtils.createTestProject(baselines.openSdkStyleSqlProjectBaseline); + let proj = await testUtils.createTestProject(this.test, baselines.openSdkStyleSqlProjectBaseline); const projTreeRoot = await setupMoveTest(proj); const projController = new ProjectsController(testContext.outputChannel); @@ -868,7 +869,7 @@ describe('ProjectsController', function (): void { it('Should only allow moving files', async function (): Promise { const spy = sinon.spy(vscode.window, 'showErrorMessage'); - let proj = await testUtils.createTestProject(baselines.openSdkStyleSqlProjectBaseline); + let proj = await testUtils.createTestProject(this.test, baselines.openSdkStyleSqlProjectBaseline); const projTreeRoot = await setupMoveTest(proj); const projController = new ProjectsController(testContext.outputChannel); @@ -902,8 +903,8 @@ describe('ProjectsController', function (): void { const spy = sinon.spy(vscode.window, 'showErrorMessage'); sinon.stub(vscode.window, 'showWarningMessage').returns(Promise.resolve(constants.move)); - let proj1 = await testUtils.createTestProject(baselines.openSdkStyleSqlProjectBaseline); - let proj2 = await testUtils.createTestProject(baselines.openSdkStyleSqlProjectBaseline); + let proj1 = await testUtils.createTestProject(this.test, baselines.openSdkStyleSqlProjectBaseline); + let proj2 = await testUtils.createTestProject(this.test, baselines.openSdkStyleSqlProjectBaseline); const projTreeRoot1 = await setupMoveTest(proj1); const projTreeRoot2 = await setupMoveTest(proj2); @@ -926,7 +927,7 @@ describe('ProjectsController', function (): void { describe('Rename file', function (): void { it('Should not do anything if no new name is provided', async function (): Promise { sinon.stub(vscode.window, 'showInputBox').resolves(''); - let proj = await testUtils.createTestProject(baselines.openSdkStyleSqlProjectBaseline); + let proj = await testUtils.createTestProject(this.test, baselines.openSdkStyleSqlProjectBaseline); const projTreeRoot = await setupMoveTest(proj); const projController = new ProjectsController(testContext.outputChannel); @@ -942,7 +943,7 @@ describe('ProjectsController', function (): void { it('Should rename a sql object file', async function (): Promise { sinon.stub(vscode.window, 'showInputBox').resolves('newName'); - let proj = await testUtils.createTestProject(baselines.openSdkStyleSqlProjectBaseline); + let proj = await testUtils.createTestProject(this.test, baselines.openSdkStyleSqlProjectBaseline); const projTreeRoot = await setupMoveTest(proj); const projController = new ProjectsController(testContext.outputChannel); @@ -957,7 +958,7 @@ describe('ProjectsController', function (): void { }); it('Should rename a pre and post deploy script', async function (): Promise { - let proj = await testUtils.createTestProject(baselines.newSdkStyleProjectSdkNodeBaseline); + let proj = await testUtils.createTestProject(this.test, baselines.newSdkStyleProjectSdkNodeBaseline); await proj.addScriptItem('Script.PreDeployment1.sql', 'pre-deployment stuff', ItemType.preDeployScript); await proj.addScriptItem('Script.PostDeployment1.sql', 'post-deployment stuff', ItemType.postDeployScript); @@ -989,7 +990,7 @@ describe('ProjectsController', function (): void { describe('SqlCmd Variables', function (): void { it('Should delete sqlcmd variable', async function (): Promise { - let project = await testUtils.createTestProject(baselines.openSdkStyleSqlProjectBaseline); + let project = await testUtils.createTestProject(this.test, baselines.openSdkStyleSqlProjectBaseline); const sqlProjectsService = await utils.getSqlProjectsService(); await sqlProjectsService.openProject(project.projectFilePath); @@ -1013,12 +1014,72 @@ describe('ProjectsController', function (): void { project = await Project.openProject(project.projectFilePath); should(Object.keys(project.sqlCmdVariables).length).equal(1, 'The project should only have 1 sqlcmd variable after deletion'); }); + + it('Should add sqlcmd variable', async function (): Promise { + let project = await testUtils.createTestProject(this.test, baselines.openSdkStyleSqlProjectBaseline); + const sqlProjectsService = await utils.getSqlProjectsService(); + await sqlProjectsService.openProject(project.projectFilePath); + + const projController = new ProjectsController(testContext.outputChannel); + const projRoot = new ProjectRootTreeItem(project); + + should(Object.keys(project.sqlCmdVariables).length).equal(2, 'The project should start with 2 sqlcmd variables'); + + const inputBoxStub = sinon.stub(vscode.window, 'showInputBox'); + inputBoxStub.resolves(''); + await projController.addSqlCmdVariable(createWorkspaceTreeItem(projRoot.children.find(x => x.friendlyName === constants.sqlcmdVariablesNodeName)!)); + + // reload project + project = await Project.openProject(project.projectFilePath); + should(Object.keys(project.sqlCmdVariables).length).equal(2, 'The project should still have 2 sqlcmd variables if no name was provided'); + + inputBoxStub.reset(); + inputBoxStub.onFirstCall().resolves('newVariable'); + inputBoxStub.onSecondCall().resolves('testValue'); + await projController.addSqlCmdVariable(createWorkspaceTreeItem(projRoot.children.find(x => x.friendlyName === constants.sqlcmdVariablesNodeName)!)); + + // reload project + project = await Project.openProject(project.projectFilePath); + should(Object.keys(project.sqlCmdVariables).length).equal(3, 'The project should have 3 sqlcmd variable after adding a new one'); + }); + + it('Should update sqlcmd variable', async function (): Promise { + let project = await testUtils.createTestProject(this.test, baselines.openSdkStyleSqlProjectBaseline); + const sqlProjectsService = await utils.getSqlProjectsService(); + await sqlProjectsService.openProject(project.projectFilePath); + + const projController = new ProjectsController(testContext.outputChannel); + const projRoot = new ProjectRootTreeItem(project); + + should(Object.keys(project.sqlCmdVariables).length).equal(2, 'The project should start with 2 sqlcmd variables'); + + const inputBoxStub = sinon.stub(vscode.window, 'showInputBox'); + inputBoxStub.resolves(''); + const sqlcmdVarToUpdate = projRoot.children.find(x => x.friendlyName === constants.sqlcmdVariablesNodeName)!.children[0]; + const originalValue = project.sqlCmdVariables[sqlcmdVarToUpdate.friendlyName]; + await projController.editSqlCmdVariable(createWorkspaceTreeItem(sqlcmdVarToUpdate)); + + // reload project + project = await Project.openProject(project.projectFilePath); + should(Object.keys(project.sqlCmdVariables).length).equal(2, 'The project should still have 2 sqlcmd variables'); + should(project.sqlCmdVariables[sqlcmdVarToUpdate.friendlyName]).equal(originalValue, 'The value of the sqlcmd variable should not have changed'); + + inputBoxStub.reset(); + const updatedValue = 'newValue'; + inputBoxStub.resolves(updatedValue); + await projController.editSqlCmdVariable(createWorkspaceTreeItem(sqlcmdVarToUpdate)); + + // reload project + project = await Project.openProject(project.projectFilePath); + should(Object.keys(project.sqlCmdVariables).length).equal(2, 'The project should still have 2 sqlcmd variables'); + should(project.sqlCmdVariables[sqlcmdVarToUpdate.friendlyName]).equal(updatedValue, 'The value of the sqlcmd variable should have been updated'); + }); }); }); async function setupDeleteExcludeTest(proj: Project): Promise<[FileProjectEntry, ProjectRootTreeItem, FileProjectEntry, FileProjectEntry, FileProjectEntry]> { - await proj.addFolderItem('UpperFolder'); - await proj.addFolderItem('UpperFolder/LowerFolder'); + await proj.addFolder('UpperFolder'); + await proj.addFolder('UpperFolder/LowerFolder'); const scriptEntry = await proj.addScriptItem('UpperFolder/LowerFolder/someScript.sql', 'not a real script'); await proj.addScriptItem('UpperFolder/LowerFolder/someOtherScript.sql', 'Also not a real script'); await proj.addScriptItem('../anotherScript.sql', 'Also not a real script'); @@ -1030,7 +1091,8 @@ async function setupDeleteExcludeTest(proj: Project): Promise<[FileProjectEntry, sinon.stub(vscode.window, 'showWarningMessage').returns(Promise.resolve(constants.yesString)); // confirm setup - should(proj.files.length).equal(5, 'number of file/folder entries'); + should(proj.files.length).equal(3, 'number of file entries'); + should(proj.folders.length).equal(2, 'number of folder entries'); should(proj.preDeployScripts.length).equal(1, 'number of pre-deployment script entries'); should(proj.postDeployScripts.length).equal(1, 'number of post-deployment script entries'); should(proj.noneDeployScripts.length).equal(1, 'number of none script entries'); @@ -1041,8 +1103,8 @@ async function setupDeleteExcludeTest(proj: Project): Promise<[FileProjectEntry, } async function setupMoveTest(proj: Project): Promise { - await proj.addFolderItem('UpperFolder'); - await proj.addFolderItem('UpperFolder/LowerFolder'); + await proj.addFolder('UpperFolder'); + await proj.addFolder('UpperFolder/LowerFolder'); await proj.addScriptItem('UpperFolder/LowerFolder/someScript.sql', 'not a real script'); await proj.addScriptItem('UpperFolder/LowerFolder/someOtherScript.sql', 'Also not a real script'); await proj.addScriptItem('../anotherScript.sql', 'Also not a real script'); diff --git a/extensions/sql-database-projects/src/test/projectTree.test.ts b/extensions/sql-database-projects/src/test/projectTree.test.ts index bb3e5cc931..b9c6a00c0a 100644 --- a/extensions/sql-database-projects/src/test/projectTree.test.ts +++ b/extensions/sql-database-projects/src/test/projectTree.test.ts @@ -21,27 +21,27 @@ describe('Project Tree tests', function (): void { const sqlprojUri = vscode.Uri.file(`${root}Fake.sqlproj`); let inputNodes: (FileNode | FolderNode)[] = [ - new SqlObjectFileNode(vscode.Uri.file(`${root}C`), sqlprojUri), - new SqlObjectFileNode(vscode.Uri.file(`${root}D`), sqlprojUri), - new FolderNode(vscode.Uri.file(`${root}Z`), sqlprojUri), - new FolderNode(vscode.Uri.file(`${root}X`), sqlprojUri), - new SqlObjectFileNode(vscode.Uri.file(`${root}B`), sqlprojUri), - new SqlObjectFileNode(vscode.Uri.file(`${root}A`), sqlprojUri), - new FolderNode(vscode.Uri.file(`${root}W`), sqlprojUri), - new FolderNode(vscode.Uri.file(`${root}Y`), sqlprojUri) + new SqlObjectFileNode(vscode.Uri.file(`${root}C`), sqlprojUri, 'C'), + new SqlObjectFileNode(vscode.Uri.file(`${root}D`), sqlprojUri, 'D'), + new FolderNode(vscode.Uri.file(`${root}Z`), sqlprojUri, 'Z'), + new FolderNode(vscode.Uri.file(`${root}X`), sqlprojUri, 'X'), + new SqlObjectFileNode(vscode.Uri.file(`${root}B`), sqlprojUri, 'B'), + new SqlObjectFileNode(vscode.Uri.file(`${root}A`), sqlprojUri, 'A'), + new FolderNode(vscode.Uri.file(`${root}W`), sqlprojUri, 'W'), + new FolderNode(vscode.Uri.file(`${root}Y`), sqlprojUri, 'Y') ]; inputNodes = inputNodes.sort(sortFileFolderNodes); const expectedNodes: (FileNode | FolderNode)[] = [ - new FolderNode(vscode.Uri.file(`${root}W`), sqlprojUri), - new FolderNode(vscode.Uri.file(`${root}X`), sqlprojUri), - new FolderNode(vscode.Uri.file(`${root}Y`), sqlprojUri), - new FolderNode(vscode.Uri.file(`${root}Z`), sqlprojUri), - new SqlObjectFileNode(vscode.Uri.file(`${root}A`), sqlprojUri), - new SqlObjectFileNode(vscode.Uri.file(`${root}B`), sqlprojUri), - new SqlObjectFileNode(vscode.Uri.file(`${root}C`), sqlprojUri), - new SqlObjectFileNode(vscode.Uri.file(`${root}D`), sqlprojUri) + new FolderNode(vscode.Uri.file(`${root}W`), sqlprojUri, 'W'), + new FolderNode(vscode.Uri.file(`${root}X`), sqlprojUri, 'X'), + new FolderNode(vscode.Uri.file(`${root}Y`), sqlprojUri, 'Y'), + new FolderNode(vscode.Uri.file(`${root}Z`), sqlprojUri, 'Z'), + new SqlObjectFileNode(vscode.Uri.file(`${root}A`), sqlprojUri, 'A'), + new SqlObjectFileNode(vscode.Uri.file(`${root}B`), sqlprojUri, 'B'), + new SqlObjectFileNode(vscode.Uri.file(`${root}C`), sqlprojUri, 'C'), + new SqlObjectFileNode(vscode.Uri.file(`${root}D`), sqlprojUri, 'D') ]; should(inputNodes.map(n => n.relativeProjectUri.path)).deepEqual(expectedNodes.map(n => n.relativeProjectUri.path)); @@ -54,18 +54,18 @@ describe('Project Tree tests', function (): void { // nested entries before explicit top-level folder entry // also, ordering of files/folders at all levels proj.files.push(proj.createFileProjectEntry(path.join('someFolder', 'bNestedTest.sql'), EntryType.File)); - proj.files.push(proj.createFileProjectEntry(path.join('someFolder', 'bNestedFolder'), EntryType.Folder)); + proj.folders.push(proj.createFileProjectEntry(path.join('someFolder', 'bNestedFolder'), EntryType.Folder)); proj.files.push(proj.createFileProjectEntry(path.join('someFolder', 'aNestedTest.sql'), EntryType.File)); - proj.files.push(proj.createFileProjectEntry(path.join('someFolder', 'aNestedFolder'), EntryType.Folder)); - proj.files.push(proj.createFileProjectEntry('someFolder', EntryType.Folder)); + proj.folders.push(proj.createFileProjectEntry(path.join('someFolder', 'aNestedFolder'), EntryType.Folder)); + proj.folders.push(proj.createFileProjectEntry('someFolder', EntryType.Folder)); // duplicate files proj.files.push(proj.createFileProjectEntry('duplicate.sql', EntryType.File)); proj.files.push(proj.createFileProjectEntry('duplicate.sql', EntryType.File)); // duplicate folders - proj.files.push(proj.createFileProjectEntry('duplicateFolder', EntryType.Folder)); - proj.files.push(proj.createFileProjectEntry('duplicateFolder', EntryType.Folder)); + proj.folders.push(proj.createFileProjectEntry('duplicateFolder', EntryType.Folder)); + proj.folders.push(proj.createFileProjectEntry('duplicateFolder', EntryType.Folder)); const tree = new ProjectRootTreeItem(proj); should(tree.children.map(x => x.relativeProjectUri.path)).deepEqual([ @@ -102,7 +102,9 @@ describe('Project Tree tests', function (): void { // nested entries before explicit top-level folder entry // also, ordering of files/folders at all levels proj.files.push(proj.createFileProjectEntry('someFolder1\\MyNestedFolder1\\MyFile1.sql', EntryType.File)); - proj.files.push(proj.createFileProjectEntry('someFolder1\\MyNestedFolder2', EntryType.Folder)); + proj.folders.push(proj.createFileProjectEntry('someFolder1\\MyNestedFolder2', EntryType.Folder)); + proj.folders.push(proj.createFileProjectEntry('someFolder1', EntryType.Folder)); + proj.folders.push(proj.createFileProjectEntry('someFolder1\\MyNestedFolder1', EntryType.Folder)); proj.files.push(proj.createFileProjectEntry('someFolder1\\MyFile2.sql', EntryType.File)); const tree = new ProjectRootTreeItem(proj); diff --git a/extensions/sql-database-projects/src/test/publishProfile.test.ts b/extensions/sql-database-projects/src/test/publishProfile.test.ts index f31b5bcce4..b9ec29a254 100644 --- a/extensions/sql-database-projects/src/test/publishProfile.test.ts +++ b/extensions/sql-database-projects/src/test/publishProfile.test.ts @@ -29,13 +29,13 @@ describe('Publish profile tests', function (): void { sinon.restore(); }); - after(async function(): Promise { + after(async function (): Promise { await testUtils.deleteGeneratedTestFolder(); }); it('Should read database name, integrated security connection string, and SQLCMD variables from publish profile', async function (): Promise { await baselines.loadBaselines(); - const profilePath = await testUtils.createTestFile(baselines.publishProfileIntegratedSecurityBaseline, 'publishProfile.publish.xml'); + const profilePath = await testUtils.createTestFile(this.test, baselines.publishProfileIntegratedSecurityBaseline, 'publishProfile.publish.xml'); const connectionResult = { connected: true, connectionId: 'connId', @@ -58,7 +58,7 @@ describe('Publish profile tests', function (): void { it('Should read database name, SQL login connection string, and SQLCMD variables from publish profile', async function (): Promise { await baselines.loadBaselines(); - const profilePath = await testUtils.createTestFile(baselines.publishProfileSqlLoginBaseline, 'publishProfile.publish.xml'); + const profilePath = await testUtils.createTestFile(this.test, baselines.publishProfileSqlLoginBaseline, 'publishProfile.publish.xml'); const connectionResult = { providerName: 'MSSQL', connectionId: 'connId', @@ -83,7 +83,7 @@ describe('Publish profile tests', function (): void { it('Should read SQLCMD variables correctly from publish profile even if DefaultValue is used', async function (): Promise { await baselines.loadBaselines(); - const profilePath = await testUtils.createTestFile(baselines.publishProfileDefaultValueBaseline, 'publishProfile.publish.xml'); + const profilePath = await testUtils.createTestFile(this.test, baselines.publishProfileDefaultValueBaseline, 'publishProfile.publish.xml'); testContext.dacFxService.setup(x => x.getOptionsFromProfile(TypeMoq.It.isAny())).returns(async () => { return Promise.resolve(mockDacFxOptionsResult); }); @@ -97,7 +97,7 @@ describe('Publish profile tests', function (): void { it('Should throw error when connecting does not work', async function (): Promise { await baselines.loadBaselines(); - const profilePath = await testUtils.createTestFile(baselines.publishProfileIntegratedSecurityBaseline, 'publishProfile.publish.xml'); + const profilePath = await testUtils.createTestFile(this.test, baselines.publishProfileIntegratedSecurityBaseline, 'publishProfile.publish.xml'); sinon.stub(azdata.connection, 'connect').throws(new Error('Could not connect')); diff --git a/extensions/sql-database-projects/src/test/testUtils.ts b/extensions/sql-database-projects/src/test/testUtils.ts index b064db67ff..b56c3b17d2 100644 --- a/extensions/sql-database-projects/src/test/testUtils.ts +++ b/extensions/sql-database-projects/src/test/testUtils.ts @@ -13,7 +13,8 @@ import should = require('should'); import { AssertionError } from 'assert'; import { Project } from '../models/project'; import { Uri } from 'vscode'; -import { exists } from '../common/utils'; +import { exists, getSqlProjectsService } from '../common/utils'; +import { ProjectType } from 'mssql'; export async function shouldThrowSpecificError(block: Function, expectedMessage: string, details?: string) { let succeeded = false; @@ -30,37 +31,52 @@ export async function shouldThrowSpecificError(block: Function, expectedMessage: } } -export async function createTestSqlProjFile(contents: string, folderPath?: string): Promise { - folderPath = folderPath ?? path.join(await generateTestFolderPath(), 'TestProject'); +export async function createTestSqlProject(test: Mocha.Runnable | undefined): Promise { + const projPath = await getTestProjectPath(test); + await (await getSqlProjectsService()).createProject(projPath, ProjectType.SdkStyle); + return await Project.openProject(projPath); +} + +export async function getTestProjectPath(test: Mocha.Runnable | undefined): Promise { + return path.join(await generateTestFolderPath(test), 'TestProject', 'TestProject.sqlproj'); +} + +export async function createTestSqlProjFile(test: Mocha.Runnable | undefined, contents: string, folderPath?: string): Promise { + folderPath = folderPath ?? path.join(await generateTestFolderPath(test), 'TestProject'); const macroDict: Record = { 'PROJECT_DSP': constants.defaultDSP }; contents = templates.macroExpansion(contents, macroDict); - return await createTestFile(contents, 'TestProject.sqlproj', folderPath); + return await createTestFile(test, contents, 'TestProject.sqlproj', folderPath); } -export async function createTestProject(contents: string, folderPath?: string): Promise { - return await Project.openProject(await createTestSqlProjFile(contents, folderPath)); +export async function createTestProject(test: Mocha.Runnable | undefined, contents: string, folderPath?: string): Promise { + return await Project.openProject(await createTestSqlProjFile(test, contents, folderPath)); } -export async function createTestDataSources(contents: string, folderPath?: string): Promise { - return await createTestFile(contents, constants.dataSourcesFileName, folderPath); +export async function createTestDataSources(test: Mocha.Runnable | undefined, contents: string, folderPath?: string): Promise { + return await createTestFile(test, contents, constants.dataSourcesFileName, folderPath); } -export async function generateTestFolderPath(): Promise { - const folderPath = path.join(generateBaseFolderName(), `TestRun_${new Date().getTime()}`); +export async function generateTestFolderPath(test: Mocha.Runnable | undefined): Promise { + const testName = test?.title === undefined ? '' : `${normalizeTestName(test?.title)}_` + const folderPath = path.join(generateBaseFolderName(), `Test_${testName}${new Date().getTime()}_${Math.floor((Math.random() * 1000))}`); await fs.mkdir(folderPath, { recursive: true }); return folderPath; } +function normalizeTestName(rawTestName: string): string { + return rawTestName.replace(/[^\w]+/g, '').substring(0, 40); // remove all non-alphanumeric characters, then trim to a reasonable length +} + export function generateBaseFolderName(): string { const folderPath = path.join(os.tmpdir(), 'ADS_Tests'); return folderPath; } -export async function createTestFile(contents: string, fileName: string, folderPath?: string): Promise { - folderPath = folderPath ?? await generateTestFolderPath(); +export async function createTestFile(test: Mocha.Runnable | undefined, contents: string, fileName: string, folderPath?: string): Promise { + folderPath = folderPath ?? await generateTestFolderPath(test); const filePath = path.join(folderPath, fileName); await fs.mkdir(path.dirname(filePath), { recursive: true }); @@ -89,13 +105,12 @@ export async function createTestFile(contents: string, fileName: string, folderP * @param createList Boolean specifying to create a list of the files and folders been created * @param list List of files and folders that are been created */ -export async function createDummyFileStructure(createList?: boolean, list?: Uri[], testFolderPath?: string): Promise { - testFolderPath = testFolderPath ?? await generateTestFolderPath(); +export async function createDummyFileStructure(test: Mocha.Runnable | undefined, createList?: boolean, list?: Uri[], testFolderPath?: string): Promise { + testFolderPath = testFolderPath ?? await generateTestFolderPath(test); let filePath = path.join(testFolderPath, 'file1.sql'); await fs.writeFile(filePath, ''); if (createList) { - list?.push(Uri.file(testFolderPath)); list?.push(Uri.file(filePath)); } @@ -103,10 +118,6 @@ export async function createDummyFileStructure(createList?: boolean, list?: Uri[ let dirName = path.join(testFolderPath, `folder${dirCount}`); await fs.mkdir(dirName, { recursive: true }); - if (createList) { - list?.push(Uri.file(dirName)); - } - for (let fileCount = 1; fileCount <= 5; fileCount++) { let fileName = path.join(dirName, `file${fileCount}.sql`); await fs.writeFile(fileName, ''); @@ -117,7 +128,7 @@ export async function createDummyFileStructure(createList?: boolean, list?: Uri[ } filePath = path.join(testFolderPath, 'file2.txt'); - //await touchFile(filePath); + await fs.writeFile(filePath, ''); if (createList) { list?.push(Uri.file(filePath)); @@ -153,8 +164,8 @@ export async function createDummyFileStructure(createList?: boolean, list?: Uri[ * @param createList Boolean specifying to create a list of the files and folders been created * @param list List of files and folders that are been created */ -export async function createDummyFileStructureWithPrePostDeployScripts(createList?: boolean, list?: Uri[], testFolderPath?: string): Promise { - testFolderPath = await createDummyFileStructure(createList, list, testFolderPath); +export async function createDummyFileStructureWithPrePostDeployScripts(test: Mocha.Runnable | undefined, createList?: boolean, list?: Uri[], testFolderPath?: string): Promise { + testFolderPath = await createDummyFileStructure(test, createList, list, testFolderPath); // add pre-deploy scripts const predeployscript1 = path.join(testFolderPath, 'Script.PreDeployment1.sql'); @@ -189,10 +200,10 @@ export async function createDummyFileStructureWithPrePostDeployScripts(createLis return testFolderPath; } -export async function createListOfFiles(filePath?: string): Promise { +export async function createListOfFiles(test: Mocha.Runnable | undefined, filePath?: string): Promise { let fileFolderList: Uri[] = []; - await createDummyFileStructure(true, fileFolderList, filePath); + await createDummyFileStructure(test, true, fileFolderList, filePath); return fileFolderList; } @@ -201,14 +212,14 @@ export async function createListOfFiles(filePath?: string): Promise { * TestFolder directory structure * - file1.sql * - folder1 - * -file1.sql - * -file2.sql - * -test1.sql - * -test2.sql - * -testLongerName.sql + * - file1.sql + * - file2.sql + * - test1.sql + * - test2.sql + * - testLongerName.sql * - folder2 - * -file1.sql - * -file2.sql + * - file1.sql + * - file2.sql * - Script.PreDeployment1.sql * - Script.PostDeployment1.sql * - Script.PostDeployment2.sql @@ -262,7 +273,6 @@ export async function createOtherDummyFiles(testFolderPath: string): Promise { const testFolderPath: string = generateBaseFolderName(); if (await exists(testFolderPath)) { - // cleanup folder - await fs.rm(testFolderPath, { recursive: true }); + await fs.rm(testFolderPath, { recursive: true }); // cleanup folder } -} \ No newline at end of file +} diff --git a/extensions/sql-database-projects/src/test/utils.test.ts b/extensions/sql-database-projects/src/test/utils.test.ts index ce3aafb89f..92e12f04b6 100644 --- a/extensions/sql-database-projects/src/test/utils.test.ts +++ b/extensions/sql-database-projects/src/test/utils.test.ts @@ -6,20 +6,22 @@ import * as should from 'should'; import * as path from 'path'; import * as os from 'os'; +import * as constants from '../common/constants'; +import * as utils from '../common/utils'; + import { createDummyFileStructure, deleteGeneratedTestFolder } from './testUtils'; -import { exists, trimUri, removeSqlCmdVariableFormatting, formatSqlCmdVariable, isValidSqlCmdVariableName, timeConversion, validateSqlServerPortNumber, isEmptyString, detectCommandInstallation, isValidSQLPassword, findSqlVersionInImageName, findSqlVersionInTargetPlatform } from '../common/utils'; import { Uri } from 'vscode'; describe('Tests to verify utils functions', function (): void { it('Should determine existence of files/folders', async () => { - let testFolderPath = await createDummyFileStructure(); + let testFolderPath = await createDummyFileStructure(undefined); - should(await exists(testFolderPath)).equal(true); - should(await exists(path.join(testFolderPath, 'file1.sql'))).equal(true); - should(await exists(path.join(testFolderPath, 'folder2'))).equal(true); - should(await exists(path.join(testFolderPath, 'folder4'))).equal(false); - should(await exists(path.join(testFolderPath, 'folder2', 'file4.sql'))).equal(true); - should(await exists(path.join(testFolderPath, 'folder4', 'file2.sql'))).equal(false); + should(await utils.exists(testFolderPath)).equal(true); + should(await utils.exists(path.join(testFolderPath, 'file1.sql'))).equal(true); + should(await utils.exists(path.join(testFolderPath, 'folder2'))).equal(true); + should(await utils.exists(path.join(testFolderPath, 'folder4'))).equal(false); + should(await utils.exists(path.join(testFolderPath, 'folder2', 'file4.sql'))).equal(true); + should(await utils.exists(path.join(testFolderPath, 'folder4', 'file2.sql'))).equal(false); await deleteGeneratedTestFolder(); }); @@ -28,117 +30,124 @@ describe('Tests to verify utils functions', function (): void { const root = os.platform() === 'win32' ? 'Z:\\' : '/'; let projectUri = Uri.file(path.join(root, 'project', 'folder', 'project.sqlproj')); let fileUri = Uri.file(path.join(root, 'project', 'folder', 'file.sql')); - should(trimUri(projectUri, fileUri)).equal('file.sql'); + should(utils.trimUri(projectUri, fileUri)).equal('file.sql'); fileUri = Uri.file(path.join(root, 'project', 'file.sql')); - let urifile = trimUri(projectUri, fileUri); + let urifile = utils.trimUri(projectUri, fileUri); should(urifile).equal('../file.sql'); fileUri = Uri.file(path.join(root, 'project', 'forked', 'file.sql')); - should(trimUri(projectUri, fileUri)).equal('../forked/file.sql'); + should(utils.trimUri(projectUri, fileUri)).equal('../forked/file.sql'); fileUri = Uri.file(path.join(root, 'forked', 'from', 'top', 'file.sql')); - should(trimUri(projectUri, fileUri)).equal('../../forked/from/top/file.sql'); + should(utils.trimUri(projectUri, fileUri)).equal('../../forked/from/top/file.sql'); }); it('Should remove $() from sqlcmd variables', () => { - should(removeSqlCmdVariableFormatting('$(test)')).equal('test', '$() surrounding the variable should have been removed'); - should(removeSqlCmdVariableFormatting('$(test')).equal('test', '$( at the beginning of the variable should have been removed'); - should(removeSqlCmdVariableFormatting('test')).equal('test', 'string should not have been changed because it is not in sqlcmd variable format'); + should(utils.removeSqlCmdVariableFormatting('$(test)')).equal('test', '$() surrounding the variable should have been removed'); + should(utils.removeSqlCmdVariableFormatting('$(test')).equal('test', '$( at the beginning of the variable should have been removed'); + should(utils.removeSqlCmdVariableFormatting('test')).equal('test', 'string should not have been changed because it is not in sqlcmd variable format'); }); it('Should make variable be in sqlcmd variable format with $()', () => { - should(formatSqlCmdVariable('$(test)')).equal('$(test)', 'string should not have been changed because it was already in the correct format'); - should(formatSqlCmdVariable('test')).equal('$(test)', 'string should have been changed to be in sqlcmd variable format'); - should(formatSqlCmdVariable('$(test')).equal('$(test)', 'string should have been changed to be in sqlcmd variable format'); - should(formatSqlCmdVariable('')).equal('', 'should not do anything to an empty string'); + should(utils.formatSqlCmdVariable('$(test)')).equal('$(test)', 'string should not have been changed because it was already in the correct format'); + should(utils.formatSqlCmdVariable('test')).equal('$(test)', 'string should have been changed to be in sqlcmd variable format'); + should(utils.formatSqlCmdVariable('$(test')).equal('$(test)', 'string should have been changed to be in sqlcmd variable format'); + should(utils.formatSqlCmdVariable('')).equal('', 'should not do anything to an empty string'); }); it('Should determine invalid sqlcmd variable names', () => { // valid names - should(isValidSqlCmdVariableName('$(test)')).equal(true); - should(isValidSqlCmdVariableName('$(test )')).equal(true, 'trailing spaces should be valid because they will be trimmed'); - should(isValidSqlCmdVariableName('test')).equal(true); - should(isValidSqlCmdVariableName('test ')).equal(true, 'trailing spaces should be valid because they will be trimmed'); - should(isValidSqlCmdVariableName('$(test')).equal(true); - should(isValidSqlCmdVariableName('$(test ')).equal(true, 'trailing spaces should be valid because they will be trimmed'); + should(utils.isValidSqlCmdVariableName('$(test)')).equal(true); + should(utils.isValidSqlCmdVariableName('$(test )')).equal(true, 'trailing spaces should be valid because they will be trimmed'); + should(utils.isValidSqlCmdVariableName('test')).equal(true); + should(utils.isValidSqlCmdVariableName('test ')).equal(true, 'trailing spaces should be valid because they will be trimmed'); + should(utils.isValidSqlCmdVariableName('$(test')).equal(true); + should(utils.isValidSqlCmdVariableName('$(test ')).equal(true, 'trailing spaces should be valid because they will be trimmed'); // whitespace - should(isValidSqlCmdVariableName('')).equal(false); - should(isValidSqlCmdVariableName(' ')).equal(false); - should(isValidSqlCmdVariableName(' ')).equal(false); - should(isValidSqlCmdVariableName('test abc')).equal(false); - should(isValidSqlCmdVariableName(' ')).equal(false); + should(utils.isValidSqlCmdVariableName('')).equal(false); + should(utils.isValidSqlCmdVariableName(' ')).equal(false); + should(utils.isValidSqlCmdVariableName(' ')).equal(false); + should(utils.isValidSqlCmdVariableName('test abc')).equal(false); + should(utils.isValidSqlCmdVariableName(' ')).equal(false); // invalid characters - should(isValidSqlCmdVariableName('$($test')).equal(false); - should(isValidSqlCmdVariableName('$test')).equal(false); - should(isValidSqlCmdVariableName('$test')).equal(false); - should(isValidSqlCmdVariableName('test@')).equal(false); - should(isValidSqlCmdVariableName('test#')).equal(false); - should(isValidSqlCmdVariableName('test"')).equal(false); - should(isValidSqlCmdVariableName('test\'')).equal(false); - should(isValidSqlCmdVariableName('test-1')).equal(false); + should(utils.isValidSqlCmdVariableName('$($test')).equal(false); + should(utils.isValidSqlCmdVariableName('$test')).equal(false); + should(utils.isValidSqlCmdVariableName('$test')).equal(false); + should(utils.isValidSqlCmdVariableName('test@')).equal(false); + should(utils.isValidSqlCmdVariableName('test#')).equal(false); + should(utils.isValidSqlCmdVariableName('test"')).equal(false); + should(utils.isValidSqlCmdVariableName('test\'')).equal(false); + should(utils.isValidSqlCmdVariableName('test-1')).equal(false); }); it('Should convert from milliseconds to hr min sec correctly', () => { - should(timeConversion((60 * 60 * 1000) + (59 * 60 * 1000) + (59 * 1000))).equal('1 hr, 59 min, 59 sec'); - should(timeConversion((60 * 60 * 1000) + (59 * 60 * 1000))).equal('1 hr, 59 min'); - should(timeConversion((60 * 60 * 1000))).equal('1 hr'); - should(timeConversion((60 * 60 * 1000) + (59 * 1000))).equal('1 hr, 59 sec'); - should(timeConversion((59 * 60 * 1000) + (59 * 1000))).equal('59 min, 59 sec'); - should(timeConversion((59 * 1000))).equal('59 sec'); - should(timeConversion((59))).equal('59 msec'); + should(utils.timeConversion((60 * 60 * 1000) + (59 * 60 * 1000) + (59 * 1000))).equal('1 hr, 59 min, 59 sec'); + should(utils.timeConversion((60 * 60 * 1000) + (59 * 60 * 1000))).equal('1 hr, 59 min'); + should(utils.timeConversion((60 * 60 * 1000))).equal('1 hr'); + should(utils.timeConversion((60 * 60 * 1000) + (59 * 1000))).equal('1 hr, 59 sec'); + should(utils.timeConversion((59 * 60 * 1000) + (59 * 1000))).equal('59 min, 59 sec'); + should(utils.timeConversion((59 * 1000))).equal('59 sec'); + should(utils.timeConversion((59))).equal('59 msec'); }); it('Should validate port number correctly', () => { - should(validateSqlServerPortNumber('invalid')).equals(false); - should(validateSqlServerPortNumber('')).equals(false); - should(validateSqlServerPortNumber(undefined)).equals(false); - should(validateSqlServerPortNumber('65536')).equals(false); - should(validateSqlServerPortNumber('-1')).equals(false); - should(validateSqlServerPortNumber('65530')).equals(true); - should(validateSqlServerPortNumber('1533')).equals(true); + should(utils.validateSqlServerPortNumber('invalid')).equals(false); + should(utils.validateSqlServerPortNumber('')).equals(false); + should(utils.validateSqlServerPortNumber(undefined)).equals(false); + should(utils.validateSqlServerPortNumber('65536')).equals(false); + should(utils.validateSqlServerPortNumber('-1')).equals(false); + should(utils.validateSqlServerPortNumber('65530')).equals(true); + should(utils.validateSqlServerPortNumber('1533')).equals(true); }); it('Should validate empty string correctly', () => { - should(isEmptyString('invalid')).equals(false); - should(isEmptyString('')).equals(true); - should(isEmptyString(undefined)).equals(true); - should(isEmptyString('65536')).equals(false); + should(utils.isEmptyString('invalid')).equals(false); + should(utils.isEmptyString('')).equals(true); + should(utils.isEmptyString(undefined)).equals(true); + should(utils.isEmptyString('65536')).equals(false); }); it('Should correctly detect present commands', async () => { - should(await detectCommandInstallation('node')).equal(true, '"node" should have been detected.'); - should(await detectCommandInstallation('bogusFakeCommand')).equal(false, '"bogusFakeCommand" should have been detected.'); + should(await utils.detectCommandInstallation('node')).equal(true, '"node" should have been detected.'); + should(await utils.detectCommandInstallation('bogusFakeCommand')).equal(false, '"bogusFakeCommand" should have been detected.'); }); it('Should validate SQL server password correctly', () => { - should(isValidSQLPassword('invalid')).equals(false, 'string with chars only is invalid password'); - should(isValidSQLPassword('')).equals(false, 'empty string is invalid password'); - should(isValidSQLPassword('65536')).equals(false, 'string with numbers only is invalid password'); - should(isValidSQLPassword('dFGj')).equals(false, 'string with lowercase and uppercase char only is invalid password'); - should(isValidSQLPassword('dj$')).equals(false, 'string with char and symbols only is invalid password'); - should(isValidSQLPassword('dF65530')).equals(false, 'string with char and numbers only is invalid password'); - should(isValidSQLPassword('dF6$30')).equals(false, 'dF6$30 is invalid password'); - should(isValidSQLPassword('dF65$530')).equals(true, 'dF65$530 is valid password'); - should(isValidSQLPassword('dFdf65$530')).equals(true, 'dF65$530 is valid password'); - should(isValidSQLPassword('av1fgh533@')).equals(true, 'dF65$530 is valid password'); + should(utils.isValidSQLPassword('invalid')).equals(false, 'string with chars only is invalid password'); + should(utils.isValidSQLPassword('')).equals(false, 'empty string is invalid password'); + should(utils.isValidSQLPassword('65536')).equals(false, 'string with numbers only is invalid password'); + should(utils.isValidSQLPassword('dFGj')).equals(false, 'string with lowercase and uppercase char only is invalid password'); + should(utils.isValidSQLPassword('dj$')).equals(false, 'string with char and symbols only is invalid password'); + should(utils.isValidSQLPassword('dF65530')).equals(false, 'string with char and numbers only is invalid password'); + should(utils.isValidSQLPassword('dF6$30')).equals(false, 'dF6$30 is invalid password'); + should(utils.isValidSQLPassword('dF65$530')).equals(true, 'dF65$530 is valid password'); + should(utils.isValidSQLPassword('dFdf65$530')).equals(true, 'dF65$530 is valid password'); + should(utils.isValidSQLPassword('av1fgh533@')).equals(true, 'dF65$530 is valid password'); }); it('findSqlVersionInImageName should return the version correctly', () => { - should(findSqlVersionInImageName('2017-CU1-ubuntu')).equals(2017, 'invalid number returned for 2017-CU1-ubuntu'); - should(findSqlVersionInImageName('2019-latest')).equals(2019, 'invalid number returned for 2019-latest'); - should(findSqlVersionInImageName('latest')).equals(undefined, 'invalid number returned for latest'); - should(findSqlVersionInImageName('latest-ubuntu')).equals(undefined, 'invalid number returned for latest-ubuntu'); - should(findSqlVersionInImageName('2017-CU20-ubuntu-16.04')).equals(2017, 'invalid number returned for 2017-CU20-ubuntu-16.04'); + should(utils.findSqlVersionInImageName('2017-CU1-ubuntu')).equals(2017, 'invalid number returned for 2017-CU1-ubuntu'); + should(utils.findSqlVersionInImageName('2019-latest')).equals(2019, 'invalid number returned for 2019-latest'); + should(utils.findSqlVersionInImageName('latest')).equals(undefined, 'invalid number returned for latest'); + should(utils.findSqlVersionInImageName('latest-ubuntu')).equals(undefined, 'invalid number returned for latest-ubuntu'); + should(utils.findSqlVersionInImageName('2017-CU20-ubuntu-16.04')).equals(2017, 'invalid number returned for 2017-CU20-ubuntu-16.04'); }); it('findSqlVersionInTargetPlatform should return the version correctly', () => { - should(findSqlVersionInTargetPlatform('SQL Server 2012')).equals(2012, 'invalid number returned for SQL Server 2012'); - should(findSqlVersionInTargetPlatform('SQL Server 2019')).equals(2019, 'invalid number returned for SQL Server 2019'); - should(findSqlVersionInTargetPlatform('Azure SQL Database')).equals(undefined, 'invalid number returned for Azure SQL Database'); - should(findSqlVersionInTargetPlatform('Azure Synapse SQL Pool')).equals(undefined, 'invalid number returned for Azure Synapse SQL Pool'); + should(utils.findSqlVersionInTargetPlatform('SQL Server 2012')).equals(2012, 'invalid number returned for SQL Server 2012'); + should(utils.findSqlVersionInTargetPlatform('SQL Server 2019')).equals(2019, 'invalid number returned for SQL Server 2019'); + should(utils.findSqlVersionInTargetPlatform('Azure SQL Database')).equals(undefined, 'invalid number returned for Azure SQL Database'); + should(utils.findSqlVersionInTargetPlatform('Azure Synapse SQL Pool')).equals(undefined, 'invalid number returned for Azure Synapse SQL Pool'); + }); + + it('Should only return well known database strings when getWellKnownDatabaseSources function is called', async function (): Promise { + const sources = ['test1', 'test2', 'test3', constants.WellKnownDatabaseSources[0]]; + + (utils.getWellKnownDatabaseSources(sources).length).should.equal(1); + (utils.getWellKnownDatabaseSources(sources)[0]).should.equal(constants.WellKnownDatabaseSources[0]); }); }); diff --git a/extensions/sql-database-projects/src/tools/buildHelper.ts b/extensions/sql-database-projects/src/tools/buildHelper.ts index acfc2b1397..a10cd1a656 100644 --- a/extensions/sql-database-projects/src/tools/buildHelper.ts +++ b/extensions/sql-database-projects/src/tools/buildHelper.ts @@ -12,6 +12,7 @@ import * as extractZip from 'extract-zip'; import * as constants from '../common/constants'; import { HttpClient } from '../common/httpClient'; import { DBProjectConfigurationKey } from './netcoreTool'; +import { ProjectType } from 'mssql'; const buildDirectory = 'BuildDirectory'; const sdkName = 'Microsoft.Build.Sql'; @@ -128,17 +129,17 @@ export class BuildHelper { return this.extensionBuildDir; } - public constructBuildArguments(projectPath: string, buildDirPath: string, isSdkStyleProject: boolean): string { + public constructBuildArguments(projectPath: string, buildDirPath: string, sqlProjStyle: ProjectType): string { projectPath = utils.getQuotedPath(projectPath); buildDirPath = utils.getQuotedPath(buildDirPath); // Right now SystemDacpacsLocation and NETCoreTargetsPath get set to the same thing, but separating them out for if we move // the system dacpacs somewhere else and also so that the variable name makes more sense if building from the commandline, // since SDK style projects don't to specify the targets path, just where the system dacpacs are - if (isSdkStyleProject) { + if (sqlProjStyle === ProjectType.SdkStyle) { return ` build ${projectPath} /p:NetCoreBuild=true /p:SystemDacpacsLocation=${buildDirPath}`; } else { - return ` build ${projectPath} /p:NetCoreBuild=true /p:NETCoreTargetsPath=${buildDirPath}`; + return ` build ${projectPath} /p:NetCoreBuild=true /p:NETCoreTargetsPath=${buildDirPath} /p:SystemDacpacsLocation=${buildDirPath}`; } } } diff --git a/extensions/types/vscode-mssql.d.ts b/extensions/types/vscode-mssql.d.ts index c057f83455..3ad7b5b01b 100644 --- a/extensions/types/vscode-mssql.d.ts +++ b/extensions/types/vscode-mssql.d.ts @@ -610,9 +610,8 @@ declare module 'vscode-mssql' { * @param projectUri Absolute path of the project, including .sqlproj * @param name Name of the SQLCMD variable * @param defaultValue Default value of the SQLCMD variable - * @param value Value of the SQLCMD variable, with or without the $() */ - addSqlCmdVariable(projectUri: string, name: string, defaultValue: string, value: string): Promise; + addSqlCmdVariable(projectUri: string, name: string, defaultValue: string): Promise; /** * Delete a SQLCMD variable from a project @@ -626,9 +625,8 @@ declare module 'vscode-mssql' { * @param projectUri Absolute path of the project, including .sqlproj * @param name Name of the SQLCMD variable * @param defaultValue Default value of the SQLCMD variable - * @param value Value of the SQLCMD variable, with or without the $() */ - updateSqlCmdVariable(projectUri: string, name: string, defaultValue: string, value: string): Promise; + updateSqlCmdVariable(projectUri: string, name: string, defaultValue: string): Promise; /** * Add a SQL object script to a project @@ -764,6 +762,14 @@ declare module 'vscode-mssql' { * Source of the database schema, used in telemetry */ databaseSource?: string; + /** + * Style of the .sqlproj file - SdkStyle or LegacyStyle + */ + projectStyle: ProjectType; + /** + * Database Schema Provider, in the format "Microsoft.Data.Tools.Schema.Sql.SqlXYZDatabaseSchemaProvider" + */ + databaseSchemaProvider: string } export interface GetDatabaseReferencesResult extends ResultStatus { @@ -1128,7 +1134,7 @@ declare module 'vscode-mssql' { } interface UserDatabaseReference extends DatabaseReference { - databaseVariable: SqlCmdVariable; + databaseVariable?: SqlCmdVariable; serverVariable?: SqlCmdVariable; }