mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-22 17:22:59 -05:00
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:
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -306,80 +306,63 @@ export class Project implements ISqlProject {
|
||||
|
||||
/**
|
||||
* Adds a folder to the project, and saves the project file
|
||||
*
|
||||
* @param relativeFolderPath Relative path of the folder
|
||||
* @param doNotThrowOnDuplicate
|
||||
* Flag that indicates whether duplicate entries should be ignored or throw an error. If flag is set to `true` and
|
||||
* item already exists in the project file, then existing entry will be returned.
|
||||
*/
|
||||
public async addFolderItem(relativeFolderPath: string, doNotThrowOnDuplicate?: boolean): Promise<FileProjectEntry> {
|
||||
const absoluteFolderPath = path.join(this.projectFolderPath, relativeFolderPath);
|
||||
const normalizedRelativeFolderPath = utils.convertSlashesForSqlProj(relativeFolderPath);
|
||||
public async addFolderItem(relativeFolderPath: string): Promise<FileProjectEntry> {
|
||||
const folderEntry = await this.ensureFolderItems(relativeFolderPath);
|
||||
|
||||
// check if folder already has been added to sqlproj
|
||||
const existingEntry = this.files.find(f => f.relativePath.toUpperCase() === normalizedRelativeFolderPath.toUpperCase());
|
||||
if (existingEntry) {
|
||||
if (!doNotThrowOnDuplicate) {
|
||||
throw new Error(constants.folderAlreadyAddedToProject((relativeFolderPath)));
|
||||
}
|
||||
|
||||
return existingEntry;
|
||||
if (folderEntry) {
|
||||
return folderEntry;
|
||||
} else {
|
||||
throw new Error(constants.outsideFolderPath);
|
||||
}
|
||||
|
||||
// If folder doesn't exist, create it
|
||||
let exists = await utils.exists(absoluteFolderPath);
|
||||
if (!exists) {
|
||||
await fs.mkdir(absoluteFolderPath, { recursive: true });
|
||||
}
|
||||
|
||||
const folderEntry = this.createFileProjectEntry(normalizedRelativeFolderPath, EntryType.Folder);
|
||||
this._files.push(folderEntry);
|
||||
|
||||
await this.addToProjFile(folderEntry);
|
||||
return folderEntry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a file to disk if contents are provided, adds that file to the project, and writes it to disk
|
||||
*
|
||||
* @param relativeFilePath Relative path of the file
|
||||
* @param contents Contents to be written to the new file
|
||||
* @param itemType Type of the project entry to add. This maps to the build action for the item.
|
||||
* @param doNotThrowOnDuplicate
|
||||
* Flag that indicates whether duplicate entries should be ignored or throw an error. If flag is set to `true` and
|
||||
* item already exists in the project file, then existing entry will be returned.
|
||||
*/
|
||||
public async addScriptItem(relativeFilePath: string, contents?: string, itemType?: string, doNotThrowOnDuplicate?: boolean): Promise<FileProjectEntry> {
|
||||
public async addScriptItem(relativeFilePath: string, contents?: string, itemType?: string): Promise<FileProjectEntry> {
|
||||
const absoluteFilePath = path.join(this.projectFolderPath, relativeFilePath);
|
||||
|
||||
// check if file already exists if content was passed to write to a new file
|
||||
if (contents !== undefined && contents !== '' && await utils.exists(absoluteFilePath)) {
|
||||
throw new Error(constants.fileAlreadyExists(path.parse(absoluteFilePath).name));
|
||||
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);
|
||||
|
||||
// check if file already has been added to sqlproj
|
||||
const existingEntry = this.files.find(f => f.relativePath.toUpperCase() === normalizedRelativeFilePath.toUpperCase());
|
||||
if (existingEntry) {
|
||||
if (!doNotThrowOnDuplicate) {
|
||||
throw new Error(constants.fileAlreadyAddedToProject((relativeFilePath)));
|
||||
}
|
||||
|
||||
return existingEntry;
|
||||
}
|
||||
|
||||
// create the file if contents were passed in
|
||||
if (contents) {
|
||||
await fs.mkdir(path.dirname(absoluteFilePath), { recursive: true });
|
||||
await fs.writeFile(absoluteFilePath, contents);
|
||||
}
|
||||
|
||||
// check that file exists
|
||||
let exists = await utils.exists(absoluteFilePath);
|
||||
if (!exists) {
|
||||
throw new Error(constants.noFileExist(absoluteFilePath));
|
||||
}
|
||||
|
||||
// update sqlproj XML
|
||||
// Update sqlproj XML
|
||||
const fileEntry = this.createFileProjectEntry(normalizedRelativeFilePath, EntryType.File);
|
||||
|
||||
let xmlTag;
|
||||
@@ -1033,10 +1016,10 @@ export class Project implements ISqlProject {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param doNotThrowOnDuplicate Flag that indicates whether duplicate entries should be ignored or throw an error.
|
||||
*/
|
||||
public async addToProject(list: Uri[], doNotThrowOnDuplicate?: boolean): Promise<void> {
|
||||
public async addToProject(list: Uri[]): Promise<void> {
|
||||
// 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);
|
||||
@@ -1053,9 +1036,9 @@ export class Project implements ISqlProject {
|
||||
const fileStat = await fs.stat(file.fsPath);
|
||||
|
||||
if (fileStat.isFile() && file.fsPath.toLowerCase().endsWith(constants.sqlFileExtension)) {
|
||||
await this.addScriptItem(relativePath, undefined, undefined, doNotThrowOnDuplicate);
|
||||
await this.addScriptItem(relativePath);
|
||||
} else if (fileStat.isDirectory()) {
|
||||
await this.addFolderItem(relativePath, doNotThrowOnDuplicate);
|
||||
await this.addFolderItem(relativePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1106,6 +1089,56 @@ export class Project implements ISqlProject {
|
||||
|
||||
return firstPropertyElement.childNodes[0].data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<FileProjectEntry | undefined> {
|
||||
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 });
|
||||
|
||||
// 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 <Folder/> item for the folder - add it
|
||||
folderEntry = this.createFileProjectEntry(folderEntryPath, EntryType.Folder);
|
||||
this.files.push(folderEntry);
|
||||
await this.addToProjFile(folderEntry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return folderEntry;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -45,30 +45,26 @@ declare module 'sqldbproj' {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param doNotThrowOnDuplicate Flag that indicates whether duplicate entries should be ignored or throw an error.
|
||||
*/
|
||||
addToProject(list: vscode.Uri[], doNotThrowOnDuplicate?: boolean): Promise<void>;
|
||||
addToProject(list: vscode.Uri[]): Promise<void>;
|
||||
|
||||
/**
|
||||
* Adds a folder to the project, and saves the project file
|
||||
*
|
||||
* @param relativeFolderPath Relative path of the folder
|
||||
* @param doNotThrowOnDuplicate
|
||||
* Flag that indicates whether duplicate entries should be ignored or throw an error. If flag is set to `true` and
|
||||
* item already exists in the project file, then existing entry will be returned.
|
||||
*/
|
||||
addFolderItem(relativeFolderPath: string, doNotThrowOnDuplicate?: boolean): Promise<IFileProjectEntry>;
|
||||
addFolderItem(relativeFolderPath: string): Promise<IFileProjectEntry>;
|
||||
|
||||
/**
|
||||
* Writes a file to disk if contents are provided, adds that file to the project, and writes it to disk
|
||||
*
|
||||
* @param relativeFilePath Relative path of the file
|
||||
* @param contents Contents to be written to the new file
|
||||
* @param itemType Type of the project entry to add. This maps to the build action for the item.
|
||||
* @param doNotThrowOnDuplicate
|
||||
* Flag that indicates whether duplicate entries should be ignored or throw an error. If flag is set to `true` and
|
||||
* item already exists in the project file, then existing entry will be returned.
|
||||
*/
|
||||
addScriptItem(relativeFilePath: string, contents?: string, itemType?: string, doNotThrowOnDuplicate?: boolean): Promise<IFileProjectEntry>;
|
||||
addScriptItem(relativeFilePath: string, contents?: string, itemType?: string): Promise<IFileProjectEntry>;
|
||||
|
||||
/**
|
||||
* Adds a SQLCMD variable to the project
|
||||
|
||||
@@ -13,7 +13,7 @@ import * as constants from '../common/constants';
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import { Project, EntryType, SystemDatabase, SystemDatabaseReferenceProjectEntry, SqlProjectReferenceProjectEntry } from '../models/project';
|
||||
import { exists, convertSlashesForSqlProj, trimChars, trimUri } from '../common/utils';
|
||||
import { exists, convertSlashesForSqlProj } from '../common/utils';
|
||||
import { Uri, window } from 'vscode';
|
||||
import { IDacpacReferenceSettings, IProjectReferenceSettings, ISystemDatabaseReferenceSettings } from '../models/IDatabaseReferenceSettings';
|
||||
import { SqlTargetPlatform } from 'sqldbproj';
|
||||
@@ -99,7 +99,7 @@ describe('Project: sqlproj content operations', function (): void {
|
||||
it('Should add Folder and Build entries to sqlproj', async function (): Promise<void> {
|
||||
const project = await Project.openProject(projFilePath);
|
||||
|
||||
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.\'';
|
||||
|
||||
@@ -582,43 +582,7 @@ describe('Project: sqlproj content operations', function (): void {
|
||||
|
||||
});
|
||||
|
||||
it('Should not allow adding duplicate file/folder entries in new sqlproj by default', async function (): Promise<void> {
|
||||
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');
|
||||
await project.addToProject([existingFolderUri]);
|
||||
|
||||
// Try adding the folder to the project again
|
||||
const folderRelativePath = trimChars(trimUri(Uri.file(projFilePath), existingFolderUri), '');
|
||||
await testUtils.shouldThrowSpecificError(async () => await project.addToProject([existingFolderUri]), constants.folderAlreadyAddedToProject(folderRelativePath));
|
||||
|
||||
// 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');
|
||||
await project.addToProject([existingFileUri]);
|
||||
|
||||
// Try adding the file to the project again
|
||||
let fileRelativePath = trimChars(trimUri(Uri.file(projFilePath), existingFileUri), '/');
|
||||
await testUtils.shouldThrowSpecificError(async () => await project.addToProject([existingFileUri]), constants.fileAlreadyAddedToProject(fileRelativePath));
|
||||
|
||||
// 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');
|
||||
await project.addToProject([existingFileUri]);
|
||||
|
||||
// Try adding the file from subfolder to the project again
|
||||
fileRelativePath = trimChars(trimUri(Uri.file(projFilePath), existingFileUri), '/');
|
||||
await testUtils.shouldThrowSpecificError(async () => await project.addToProject([existingFileUri]), constants.fileAlreadyAddedToProject(fileRelativePath));
|
||||
});
|
||||
|
||||
it('Should ignore duplicate file/folder entries in new sqlproj if requested', async function (): Promise<void> {
|
||||
it('Should ignore duplicate file/folder entries in new sqlproj', async function (): Promise<void> {
|
||||
projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline);
|
||||
const project: Project = await Project.openProject(projFilePath);
|
||||
const fileList = await testUtils.createListOfFiles(path.dirname(projFilePath));
|
||||
@@ -632,7 +596,7 @@ describe('Project: sqlproj content operations', function (): void {
|
||||
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], true))
|
||||
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');
|
||||
|
||||
@@ -645,7 +609,7 @@ describe('Project: sqlproj content operations', function (): void {
|
||||
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], true))
|
||||
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');
|
||||
|
||||
@@ -658,12 +622,12 @@ describe('Project: sqlproj content operations', function (): void {
|
||||
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], true))
|
||||
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 not allow adding duplicate file entries in existing sqlproj by default', async function (): Promise<void> {
|
||||
it('Should ignore duplicate file entries in existing sqlproj', async function (): Promise<void> {
|
||||
// Create new sqlproj
|
||||
projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline);
|
||||
const fileList = await testUtils.createListOfFiles(path.dirname(projFilePath));
|
||||
@@ -680,53 +644,186 @@ describe('Project: sqlproj content operations', function (): void {
|
||||
project = await Project.openProject(projFilePath);
|
||||
|
||||
// Try adding the same file to the project again
|
||||
const fileRelativePath = trimChars(trimUri(Uri.file(projFilePath), existingFileUri), '/');
|
||||
await testUtils.shouldThrowSpecificError(async () => await project.addToProject([existingFileUri]), constants.fileAlreadyAddedToProject(fileRelativePath));
|
||||
await project.addToProject([existingFileUri]);
|
||||
});
|
||||
|
||||
it('Should ignore duplicate file entries in existing sqlproj if requested', async function (): Promise<void> {
|
||||
it('Should not overwrite existing files', async function (): Promise<void> {
|
||||
// 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
|
||||
// Add a file entry to the project with explicit content
|
||||
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], true);
|
||||
const relativePath = path.relative(path.dirname(projFilePath), existingFileUri.fsPath);
|
||||
await testUtils.shouldThrowSpecificError(
|
||||
async () => await project.addScriptItem(relativePath, 'Hello World!'),
|
||||
`A file with the name '${path.parse(relativePath).name}' already exists on disk at this location. Please choose another name.`);
|
||||
});
|
||||
|
||||
it('Project entry relative path should not change after round-trip', async function (): Promise<void> {
|
||||
it('Should not add folders outside of the project folder', async function (): Promise<void> {
|
||||
// Create new sqlproj
|
||||
projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline);
|
||||
const fileList = await testUtils.createListOfFiles(path.dirname(projFilePath));
|
||||
|
||||
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))]);
|
||||
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)))]),
|
||||
'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<void> {
|
||||
// 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
|
||||
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]);
|
||||
await project.addToProject([Uri.file(newFile)]);
|
||||
|
||||
// Store the original `relativePath` of the project entry
|
||||
should(project.files.length).equal(1, 'An entry should be created in the project');
|
||||
const originalRelativePath = project.files[0].relativePath;
|
||||
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);
|
||||
|
||||
// Try adding the same file to the project again
|
||||
should(project.files.length).equal(1, 'Single entry is expected in the loaded project');
|
||||
should(project.files[0].relativePath).equal(originalRelativePath, 'Relative path should match after a round-trip');
|
||||
// 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<void> {
|
||||
// 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<void> {
|
||||
// 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<void> {
|
||||
// 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<void> {
|
||||
// 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' }]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -192,7 +192,7 @@ describe('ProjectsController', function (): void {
|
||||
|
||||
// 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.files[0].relativePath).equal('UpperFolder\\');
|
||||
should(proj.preDeployScripts.length).equal(0);
|
||||
should(proj.postDeployScripts.length).equal(0);
|
||||
should(proj.noneDeployScripts.length).equal(0);
|
||||
@@ -253,7 +253,7 @@ describe('ProjectsController', function (): void {
|
||||
|
||||
// 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.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);
|
||||
|
||||
Reference in New Issue
Block a user