Automatically add intermediate folders for SQL project items. (#16332)

* Automatically add intermediate folders for SQL project items.

While using the SQL database projects through the API, I noticed that project may end up in somewhat inconsistent state, where files will be added to the project, but their parent folders will not. This in turn resulted in failure to remove these folders from project - they will show up in the UI tree, but deleting them will cause an error. In order to align with how Visual Studio manages the projects, this change will ensure that all intermediate folders are present in the project, when new files or folders are added.

While this change improves project "correctness" when accessing it through SQL projects extension APIs, there is still a possibility that someone will open an "incorrect" previously created project. This change does not address it and folder removal may still fail.

* Update the code to never throw on duplicate items when adding files and folders to project.

After a conversation with the sqlproj owners, we agreed that there are no scenarios that would prompt us to throw an error, if duplicate item is being added to the project. Ultimately, the goal of such a request would be to have an item in the project file, which is already present, therefore the call becomes a no-op.

This allowed me to simplify the new code that was ensuring all intermediate folders are present in the project when adding files and folders.
This commit is contained in:
Alexander Ivanov
2021-08-03 09:49:11 -07:00
committed by GitHub
parent 052cb54199
commit b35e78a07f
6 changed files with 279 additions and 137 deletions

View File

@@ -195,7 +195,7 @@ export const invalidDataSchemaProvider = localize('invalidDataSchemaProvider', "
export const invalidDatabaseReference = localize('invalidDatabaseReference', "Invalid database reference in .sqlproj file");
export const databaseSelectionRequired = localize('databaseSelectionRequired', "Database selection is required to create a project from a database");
export const databaseReferenceAlreadyExists = localize('databaseReferenceAlreadyExists', "A reference to this database already exists in this project");
export const ousiderFolderPath = localize('outsideFolderPath', "Items with absolute path outside project folder are not supported. Please make sure the paths in the project file are relative to project folder.");
export const outsideFolderPath = localize('outsideFolderPath', "Items with absolute path outside project folder are not supported. Please make sure the paths in the project file are relative to project folder.");
export const parentTreeItemUnknown = localize('parentTreeItemUnknown', "Cannot access parent of provided tree item");
export const prePostDeployCount = localize('prePostDeployCount', "To successfully build, update the project to have one pre-deployment script and/or one post-deployment script");
export const invalidProjectReload = localize('invalidProjectReload', "Cannot access provided database project. Only valid, open database projects can be reloaded.");
@@ -207,8 +207,6 @@ export function fileOrFolderDoesNotExist(name: string) { return localize('fileOr
export function cannotResolvePath(path: string) { return localize('cannotResolvePath', "Cannot resolve path {0}", path); }
export function fileAlreadyExists(filename: string) { return localize('fileAlreadyExists', "A file with the name '{0}' already exists on disk at this location. Please choose another name.", filename); }
export function folderAlreadyExists(filename: string) { return localize('folderAlreadyExists', "A folder with the name '{0}' already exists on disk at this location. Please choose another name.", filename); }
export function fileAlreadyAddedToProject(filepath: string) { return localize('fileAlreadyAddedToProject', "A file with the path '{0}' has already been added to the project", filepath); }
export function folderAlreadyAddedToProject(folderpath: string) { return localize('folderAlreadyAddedToProject', "A folder with the path '{0}' has already been added to the project", folderpath); }
export function invalidInput(input: string) { return localize('invalidInput', "Invalid input: {0}", input); }
export function invalidProjectPropertyValue(propertyName: string) { return localize('invalidPropertyValue', "Invalid value specified for the property '{0}' in .sqlproj file", propertyName); }
export function unableToCreatePublishConnection(input: string) { return localize('unableToCreatePublishConnection', "Unable to construct connection: {0}", input); }
@@ -312,6 +310,12 @@ 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.
*/
export const SqlProjPathSeparator = '\\';
// Profile XML names
export const targetDatabaseName = 'TargetDatabaseName';
export const targetConnectionString = 'TargetConnectionString';

View File

@@ -38,7 +38,7 @@ export function trimUri(innerUri: vscode.Uri, outerUri: vscode.Uri): string {
if (path.isAbsolute(outerUri.path)
&& innerParts.length > 0 && outerParts.length > 0
&& innerParts[0].toLowerCase() !== outerParts[0].toLowerCase()) {
throw new Error(constants.ousiderFolderPath);
throw new Error(constants.outsideFolderPath);
}
while (innerParts.length > 0 && outerParts.length > 0 && innerParts[0].toLocaleLowerCase() === outerParts[0].toLocaleLowerCase()) {
@@ -71,6 +71,18 @@ export function trimChars(input: string, chars: string): string {
return output;
}
/**
* Ensures that folder path terminates with the slash.
* By default SSDT-style slash (`\`) is used.
*
* @param path Folder path to ensure trailing slash for.
* @param slashCharacter Slash character to ensure is present at the end of the path.
* @returns Path that ends with the given slash character.
*/
export function ensureTrailingSlash(path: string, slashCharacter: string = constants.SqlProjPathSeparator): string {
return path.endsWith(slashCharacter) ? path : path + slashCharacter;
}
/**
* Checks if the folder or file exists @param path path of the folder/file
*/
@@ -123,13 +135,13 @@ export function getPlatformSafeFileEntryPath(filePath: string): string {
}
/**
* Standardizes slashes to be "\\" for consistency between platforms and compatibility with SSDT
* Standardizes slashes to be "\" for consistency between platforms and compatibility with SSDT
*
* @param filePath Path to the file of folder.
*/
export function convertSlashesForSqlProj(filePath: string): string {
return filePath.includes('/')
? filePath.split('/').join('\\')
? filePath.split('/').join(constants.SqlProjPathSeparator)
: filePath;
}