diff --git a/extensions/data-workspace/src/common/dataWorkspaceExtension.ts b/extensions/data-workspace/src/common/dataWorkspaceExtension.ts index b95b2aa104..e09f0bdee2 100644 --- a/extensions/data-workspace/src/common/dataWorkspaceExtension.ts +++ b/extensions/data-workspace/src/common/dataWorkspaceExtension.ts @@ -31,5 +31,4 @@ export class DataWorkspaceExtension implements IExtension { validateWorkspace(): Promise { return this.workspaceService.validateWorkspace(); } - } diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index 8ed60fdc20..e1f9d35b99 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -170,6 +170,7 @@ export const externalStreamingJobValidationPassed = localize('externalStreamingJ export function projectAlreadyOpened(path: string) { return localize('projectAlreadyOpened', "Project '{0}' is already opened.", path); } export function projectAlreadyExists(name: string, path: string) { return localize('projectAlreadyExists', "A project named {0} already exists in {1}.", name, path); } export function noFileExist(fileName: string) { return localize('noFileExist', "File {0} doesn't exist", fileName); } +export function fileOrFolderDoesNotExist(name: string) { return localize('fileOrFolderDoesNotExist', "File or directory '{0}' doesn't exist", name); } 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); } diff --git a/extensions/sql-database-projects/src/controllers/mainController.ts b/extensions/sql-database-projects/src/controllers/mainController.ts index 2595167c66..806384c8d6 100644 --- a/extensions/sql-database-projects/src/controllers/mainController.ts +++ b/extensions/sql-database-projects/src/controllers/mainController.ts @@ -11,7 +11,7 @@ import * as path from 'path'; import { ProjectsController } from './projectController'; import { NetCoreTool } from '../tools/netcoreTool'; import { IconPathHelper } from '../common/iconHelper'; -import { IProjectProvider, WorkspaceTreeItem } from 'dataworkspace'; +import { WorkspaceTreeItem } from 'dataworkspace'; import { SqlDatabaseProjectProvider } from '../projectProvider/projectProvider'; /** @@ -37,7 +37,7 @@ export default class MainController implements vscode.Disposable { public deactivate(): void { } - public async activate(): Promise { + public async activate(): Promise { await this.initializeDatabaseProjects(); return new SqlDatabaseProjectProvider(this.projectsController); } diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index 7fb31ff756..34e1c5d3f7 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -768,7 +768,7 @@ export class ProjectsController { const project = await Project.openProject(newProjFilePath); await this.createProjectFromDatabaseApiCall(model); // Call ExtractAPI in DacFx Service - let fileFolderList: string[] = model.extractTarget === mssql.ExtractTarget.file ? [model.filePath] : await this.generateList(model.filePath); // Create a list of all the files and directories to be added to project + 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 await project.addToProject(fileFolderList); // Add generated file structure to the project @@ -811,8 +811,8 @@ export class ProjectsController { /** * Generate a flat list of all files and folder under a folder. */ - public async generateList(absolutePath: string): Promise { - let fileFolderList: string[] = []; + public async generateList(absolutePath: string): Promise { + let fileFolderList: vscode.Uri[] = []; if (!await utils.exists(absolutePath)) { if (await utils.exists(absolutePath + constants.sqlFileExtension)) { @@ -831,13 +831,13 @@ export class ProjectsController { const stat = await fs.stat(filepath); if (stat.isDirectory()) { - fileFolderList.push(filepath); + 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(filepath); + fileFolderList.push(vscode.Uri.file(filepath)); } } diff --git a/extensions/sql-database-projects/src/extension.ts b/extensions/sql-database-projects/src/extension.ts index 1a9123b32b..b790626091 100644 --- a/extensions/sql-database-projects/src/extension.ts +++ b/extensions/sql-database-projects/src/extension.ts @@ -5,11 +5,11 @@ import * as vscode from 'vscode'; import MainController from './controllers/mainController'; -import { IProjectProvider } from 'dataworkspace'; +import { SqlDatabaseProjectProvider } from './projectProvider/projectProvider'; let controllers: MainController[] = []; -export function activate(context: vscode.ExtensionContext): Promise { +export function activate(context: vscode.ExtensionContext): Promise { // Start the main controller const mainController = new MainController(context); controllers.push(mainController); diff --git a/extensions/sql-database-projects/src/models/project.ts b/extensions/sql-database-projects/src/models/project.ts index de1d822ff7..369016dde7 100644 --- a/extensions/sql-database-projects/src/models/project.ts +++ b/extensions/sql-database-projects/src/models/project.ts @@ -285,20 +285,20 @@ export class Project { * @param contents Contents to be written to the new file */ public async addScriptItem(relativeFilePath: string, contents?: string, itemType?: string): Promise { - // check if file already exists 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)); } - // create the file + // 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 was successfully created + // check that file exists let exists = await utils.exists(absoluteFilePath); if (!exists) { throw new Error(constants.noFileExist(absoluteFilePath)); @@ -928,21 +928,27 @@ export class Project { /** * Adds the list of sql files and directories to the project, and saves the project file - * @param absolutePath Absolute path of the folder + * @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: string[]): Promise { + 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); - for (let i = 0; i < list.length; i++) { - let file: string = list[i]; - const relativePath = utils.trimChars(utils.trimUri(Uri.file(this.projectFilePath), Uri.file(file)), '/'); + 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) { - let fileStat = await fs.stat(file); + const fileStat = await fs.stat(file.fsPath); - if (fileStat.isFile() && file.toLowerCase().endsWith(constants.sqlFileExtension)) { + if (fileStat.isFile() && file.fsPath.toLowerCase().endsWith(constants.sqlFileExtension)) { await this.addScriptItem(relativePath); - } - else if (fileStat.isDirectory()) { + } else if (fileStat.isDirectory()) { await this.addFolderItem(relativePath); } } diff --git a/extensions/sql-database-projects/src/projectProvider/projectProvider.ts b/extensions/sql-database-projects/src/projectProvider/projectProvider.ts index 2b44f5a914..232cf7aa25 100644 --- a/extensions/sql-database-projects/src/projectProvider/projectProvider.ts +++ b/extensions/sql-database-projects/src/projectProvider/projectProvider.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dataworkspace from 'dataworkspace'; +import * as sqldbproj from 'sqldbproj'; import * as vscode from 'vscode'; import * as constants from '../common/constants'; import { IconPathHelper } from '../common/iconHelper'; @@ -12,7 +13,7 @@ import { ProjectsController } from '../controllers/projectController'; import { Project } from '../models/project'; import { BaseProjectTreeItem } from '../models/tree/baseTreeItem'; -export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvider { +export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvider, sqldbproj.IExtension { constructor(private projectController: ProjectsController) { } @@ -74,4 +75,14 @@ export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvide return vscode.Uri.file(projectFile); } + + /** + * Adds the list of files and directories to the project, and saves the project file + * @param projectFile The Uri of the project file + * @param list list of uris of files and folders to add. Files and folders must already exist. Files and folders must already exist. No files or folders will be added if any do not exist. + */ + async addToProject(projectFile: vscode.Uri, list: vscode.Uri[]): Promise { + const project = await Project.openProject(projectFile.fsPath); + await project.addToProject(list); + } } diff --git a/extensions/sql-database-projects/src/sqldbproj.d.ts b/extensions/sql-database-projects/src/sqldbproj.d.ts new file mode 100644 index 0000000000..a490ee7208 --- /dev/null +++ b/extensions/sql-database-projects/src/sqldbproj.d.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'sqldbproj' { + import * as vscode from 'vscode'; + export const enum extension { + name = 'Microsoft.sql-database-projects' + } + + /** + * sql database projects extension + */ + export interface IExtension { + /** + * Create a project + * @param name name of the project + * @param location the parent directory + * @param projectTypeId the ID of the project/template + * @returns Uri of the newly created project file + */ + createProject(name: string, location: vscode.Uri, projectTypeId: string): Promise; + + /** + * Adds the list of files and directories to the project, and saves the project file + * @param projectFile The Uri of the project file + * @param list list of uris of files and folders to add. Files and folders must already exist. No files or folders will be added if any do not exist. + */ + addToProject(projectFile: vscode.Uri, list: vscode.Uri[]): Promise; + } +} diff --git a/extensions/sql-database-projects/src/test/project.test.ts b/extensions/sql-database-projects/src/test/project.test.ts index d2f79222e3..0a4ad9fa72 100644 --- a/extensions/sql-database-projects/src/test/project.test.ts +++ b/extensions/sql-database-projects/src/test/project.test.ts @@ -125,7 +125,7 @@ describe('Project: sqlproj content operations', function (): void { projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); const project = await Project.openProject(projFilePath); - let list: string[] = await testUtils.createListOfFiles(path.dirname(projFilePath)); + let list: Uri[] = await testUtils.createListOfFiles(path.dirname(projFilePath)); await project.addToProject(list); @@ -137,13 +137,13 @@ describe('Project: sqlproj content operations', function (): void { projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); const project = await Project.openProject(projFilePath); - let list: string[] = []; + let list: Uri[] = []; let testFolderPath: string = await testUtils.createDummyFileStructure(true, list, path.dirname(projFilePath)); const nonexistentFile = path.join(testFolderPath, 'nonexistentFile.sql'); - list.push(nonexistentFile); + list.push(Uri.file(nonexistentFile)); - await testUtils.shouldThrowSpecificError(async () => await project.addToProject(list), `ENOENT: no such file or directory, stat \'${nonexistentFile}\'`); + await testUtils.shouldThrowSpecificError(async () => await project.addToProject(list), constants.fileOrFolderDoesNotExist(Uri.file(nonexistentFile).fsPath)); }); it('Should choose correct master dacpac', async function (): Promise { diff --git a/extensions/sql-database-projects/src/test/testUtils.ts b/extensions/sql-database-projects/src/test/testUtils.ts index 31cc943191..b446fd2749 100644 --- a/extensions/sql-database-projects/src/test/testUtils.ts +++ b/extensions/sql-database-projects/src/test/testUtils.ts @@ -11,6 +11,7 @@ import { promises as fs } from 'fs'; import should = require('should'); import { AssertionError } from 'assert'; import { Project } from '../models/project'; +import { Uri } from 'vscode'; export async function shouldThrowSpecificError(block: Function, expectedMessage: string, details?: string) { let succeeded = false; @@ -77,14 +78,14 @@ 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?: string[], testFolderPath?: string): Promise { +export async function createDummyFileStructure(createList?: boolean, list?: Uri[], testFolderPath?: string): Promise { testFolderPath = testFolderPath ?? await generateTestFolderPath(); let filePath = path.join(testFolderPath, 'file1.sql'); await fs.writeFile(filePath, ''); if (createList) { - list?.push(testFolderPath); - list?.push(filePath); + list?.push(Uri.file(testFolderPath)); + list?.push(Uri.file(filePath)); } for (let dirCount = 1; dirCount <= 2; dirCount++) { @@ -92,14 +93,14 @@ export async function createDummyFileStructure(createList?: boolean, list?: stri await fs.mkdir(dirName, { recursive: true }); if (createList) { - list?.push(dirName); + list?.push(Uri.file(dirName)); } for (let fileCount = 1; fileCount <= 5; fileCount++) { let fileName = path.join(dirName, `file${fileCount}.sql`); await fs.writeFile(fileName, ''); if (createList) { - list?.push(fileName); + list?.push(Uri.file(fileName)); } } } @@ -108,14 +109,14 @@ export async function createDummyFileStructure(createList?: boolean, list?: stri //await touchFile(filePath); await fs.writeFile(filePath, ''); if (createList) { - list?.push(filePath); + list?.push(Uri.file(filePath)); } return testFolderPath; } -export async function createListOfFiles(filePath?: string): Promise { - let fileFolderList: string[] = []; +export async function createListOfFiles(filePath?: string): Promise { + let fileFolderList: Uri[] = []; await createDummyFileStructure(true, fileFolderList, filePath);