Expose sql project apis (#15239)

* make project variables private and add getters

* expose project in sqlproj.d.ts

* add more comments

* change to ISqlProject
This commit is contained in:
Kim Santiago
2021-04-27 14:35:27 -10:00
committed by GitHub
parent f29d7308ff
commit da37cf6ec6
3 changed files with 204 additions and 75 deletions

View File

@@ -12,6 +12,7 @@ import * as os from 'os';
import * as templates from '../templates/templates';
import { Uri, window } from 'vscode';
import { IFileProjectEntry, ISqlProject } from 'sqldbproj';
import { promises as fs } from 'fs';
import { DataSource } from './dataSources/dataSources';
import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings, IProjectReferenceSettings } from './IDatabaseReferenceSettings';
@@ -20,32 +21,76 @@ import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/t
/**
* Class representing a Project, and providing functions for operating on it
*/
export class Project {
public projectFilePath: string;
public projectFileName: string;
public projectGuid: string | undefined;
public files: FileProjectEntry[] = [];
public dataSources: DataSource[] = [];
public importedTargets: string[] = [];
public databaseReferences: IDatabaseReferenceProjectEntry[] = [];
public sqlCmdVariables: Record<string, string> = {};
public preDeployScripts: FileProjectEntry[] = [];
public postDeployScripts: FileProjectEntry[] = [];
public noneDeployScripts: FileProjectEntry[] = [];
export class Project implements ISqlProject {
private _projectFilePath: string;
private _projectFileName: string;
private _projectGuid: string | undefined;
private _files: FileProjectEntry[] = [];
private _dataSources: DataSource[] = [];
private _importedTargets: string[] = [];
private _databaseReferences: IDatabaseReferenceProjectEntry[] = [];
private _sqlCmdVariables: Record<string, string> = {};
private _preDeployScripts: FileProjectEntry[] = [];
private _postDeployScripts: FileProjectEntry[] = [];
private _noneDeployScripts: FileProjectEntry[] = [];
public get dacpacOutputPath(): string {
return path.join(this.projectFolderPath, 'bin', 'Debug', `${this.projectFileName}.dacpac`);
return path.join(this.projectFolderPath, 'bin', 'Debug', `${this._projectFileName}.dacpac`);
}
public get projectFolderPath() {
return Uri.file(path.dirname(this.projectFilePath)).fsPath;
return Uri.file(path.dirname(this._projectFilePath)).fsPath;
}
public get projectFilePath(): string {
return this._projectFilePath;
}
public get projectFileName(): string {
return this._projectFileName;
}
public get projectGuid(): string | undefined {
return this._projectGuid;
}
public get files(): FileProjectEntry[] {
return this._files;
}
public get dataSources(): DataSource[] {
return this._dataSources;
}
public get importedTargets(): string[] {
return this._importedTargets;
}
public get databaseReferences(): IDatabaseReferenceProjectEntry[] {
return this._databaseReferences;
}
public get sqlCmdVariables(): Record<string, string> {
return this._sqlCmdVariables;
}
public get preDeployScripts(): FileProjectEntry[] {
return this._preDeployScripts;
}
public get postDeployScripts(): FileProjectEntry[] {
return this._postDeployScripts;
}
public get noneDeployScripts(): FileProjectEntry[] {
return this._noneDeployScripts;
}
private projFileXmlDoc: any = undefined;
constructor(projectFilePath: string) {
this.projectFilePath = projectFilePath;
this.projectFileName = path.basename(projectFilePath, '.sqlproj');
this._projectFilePath = projectFilePath;
this._projectFileName = path.basename(projectFilePath, '.sqlproj');
}
/**
@@ -65,11 +110,11 @@ export class Project {
public async readProjFile(): Promise<void> {
this.resetProject();
const projFileText = await fs.readFile(this.projectFilePath);
const projFileText = await fs.readFile(this._projectFilePath);
this.projFileXmlDoc = new xmldom.DOMParser().parseFromString(projFileText.toString());
// get projectGUID
this.projectGuid = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ProjectGuid)[0].childNodes[0].nodeValue;
this._projectGuid = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ProjectGuid)[0].childNodes[0].nodeValue;
// find all folders and files to include
for (let ig = 0; ig < this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ItemGroup).length; ig++) {
@@ -77,14 +122,14 @@ export class Project {
const buildElements = itemGroup.getElementsByTagName(constants.Build);
for (let b = 0; b < buildElements.length; b++) {
this.files.push(this.createFileProjectEntry(buildElements[b].getAttribute(constants.Include), EntryType.File, buildElements[b].getAttribute(constants.Type)));
this._files.push(this.createFileProjectEntry(buildElements[b].getAttribute(constants.Include), EntryType.File, buildElements[b].getAttribute(constants.Type)));
}
const folderElements = itemGroup.getElementsByTagName(constants.Folder);
for (let f = 0; f < folderElements.length; f++) {
// don't add Properties folder since it isn't supported for now
if (folderElements[f].getAttribute(constants.Include) !== constants.Properties) {
this.files.push(this.createFileProjectEntry(folderElements[f].getAttribute(constants.Include), EntryType.Folder));
this._files.push(this.createFileProjectEntry(folderElements[f].getAttribute(constants.Include), EntryType.Folder));
}
}
@@ -92,7 +137,7 @@ export class Project {
let preDeployScriptCount: number = 0;
const preDeploy = itemGroup.getElementsByTagName(constants.PreDeploy);
for (let pre = 0; pre < preDeploy.length; pre++) {
this.preDeployScripts.push(this.createFileProjectEntry(preDeploy[pre].getAttribute(constants.Include), EntryType.File));
this._preDeployScripts.push(this.createFileProjectEntry(preDeploy[pre].getAttribute(constants.Include), EntryType.File));
preDeployScriptCount++;
}
@@ -100,7 +145,7 @@ export class Project {
let postDeployScriptCount: number = 0;
const postDeploy = itemGroup.getElementsByTagName(constants.PostDeploy);
for (let post = 0; post < postDeploy.length; post++) {
this.postDeployScripts.push(this.createFileProjectEntry(postDeploy[post].getAttribute(constants.Include), EntryType.File));
this._postDeployScripts.push(this.createFileProjectEntry(postDeploy[post].getAttribute(constants.Include), EntryType.File));
postDeployScriptCount++;
}
@@ -111,7 +156,7 @@ export class Project {
// find all none-deployment scripts to include
const noneItems = itemGroup.getElementsByTagName(constants.None);
for (let n = 0; n < noneItems.length; n++) {
this.noneDeployScripts.push(this.createFileProjectEntry(noneItems[n].getAttribute(constants.Include), EntryType.File));
this._noneDeployScripts.push(this.createFileProjectEntry(noneItems[n].getAttribute(constants.Include), EntryType.File));
}
}
@@ -119,11 +164,11 @@ export class Project {
const importElements = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.Import);
for (let i = 0; i < importElements.length; i++) {
const importTarget = importElements[i];
this.importedTargets.push(importTarget.getAttribute(constants.Project));
this._importedTargets.push(importTarget.getAttribute(constants.Project));
}
// find all SQLCMD variables to include
this.sqlCmdVariables = utils.readSqlCmdVariables(this.projFileXmlDoc);
this._sqlCmdVariables = utils.readSqlCmdVariables(this.projFileXmlDoc);
// find all database references to include
const references = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ArtifactReference);
@@ -142,13 +187,13 @@ export class Project {
const path = utils.convertSlashesForSqlProj(this.getSystemDacpacUri(`${name}.dacpac`).fsPath);
if (path.includes(filepath)) {
this.databaseReferences.push(new SystemDatabaseReferenceProjectEntry(
this._databaseReferences.push(new SystemDatabaseReferenceProjectEntry(
Uri.file(filepath),
this.getSystemDacpacSsdtUri(`${name}.dacpac`),
name,
suppressMissingDependencies));
} else {
this.databaseReferences.push(new DacpacReferenceProjectEntry({
this._databaseReferences.push(new DacpacReferenceProjectEntry({
dacpacFileLocation: Uri.file(utils.getPlatformSafeFileEntryPath(filepath)),
databaseName: name,
suppressMissingDependenciesErrors: suppressMissingDependencies
@@ -171,7 +216,7 @@ export class Project {
const suppressMissingDependenciesErrorNode = projectReferences[r].getElementsByTagName(constants.SuppressMissingDependenciesErrors);
const suppressMissingDependencies = suppressMissingDependenciesErrorNode[0].childNodes[0].nodeValue === true ?? false;
this.databaseReferences.push(new SqlProjectReferenceProjectEntry({
this._databaseReferences.push(new SqlProjectReferenceProjectEntry({
projectRelativePath: Uri.file(utils.getPlatformSafeFileEntryPath(filepath)),
projectName: name,
projectGuid: '', // don't care when just reading project as a reference
@@ -181,27 +226,27 @@ export class Project {
}
private resetProject(): void {
this.files = [];
this.importedTargets = [];
this.databaseReferences = [];
this.sqlCmdVariables = {};
this.preDeployScripts = [];
this.postDeployScripts = [];
this.noneDeployScripts = [];
this._files = [];
this._importedTargets = [];
this._databaseReferences = [];
this._sqlCmdVariables = {};
this._preDeployScripts = [];
this._postDeployScripts = [];
this._noneDeployScripts = [];
this.projFileXmlDoc = undefined;
}
public async updateProjectForRoundTrip(): Promise<void> {
if (this.importedTargets.includes(constants.NetCoreTargets) && !this.containsSSDTOnlySystemDatabaseReferences()) {
if (this._importedTargets.includes(constants.NetCoreTargets) && !this.containsSSDTOnlySystemDatabaseReferences()) {
return;
}
TelemetryReporter.sendActionEvent(TelemetryViews.ProjectController, TelemetryActions.updateProjectForRoundtrip);
if (!this.importedTargets.includes(constants.NetCoreTargets)) {
if (!this._importedTargets.includes(constants.NetCoreTargets)) {
const result = await window.showWarningMessage(constants.updateProjectForRoundTrip, constants.yesString, constants.noString);
if (result === constants.yesString) {
await fs.copyFile(this.projectFilePath, this.projectFilePath + '_backup');
await fs.copyFile(this._projectFilePath, this._projectFilePath + '_backup');
await this.updateImportToSupportRoundTrip();
await this.updatePackageReferenceInProjFile();
await this.updateBeforeBuildTargetInProjFile();
@@ -210,7 +255,7 @@ export class Project {
} else if (this.containsSSDTOnlySystemDatabaseReferences()) {
const result = await window.showWarningMessage(constants.updateProjectDatabaseReferencesForRoundTrip, constants.yesString, constants.noString);
if (result === constants.yesString) {
await fs.copyFile(this.projectFilePath, this.projectFilePath + '_backup');
await fs.copyFile(this._projectFilePath, this._projectFilePath + '_backup');
await this.updateSystemDatabaseReferencesInProjFile();
}
}
@@ -278,7 +323,7 @@ export class Project {
}
const folderEntry = this.createFileProjectEntry(relativeFolderPath, EntryType.Folder);
this.files.push(folderEntry);
this._files.push(folderEntry);
await this.addToProjFile(folderEntry);
return folderEntry;
@@ -321,15 +366,15 @@ export class Project {
switch (itemType) {
case templates.preDeployScript:
xmlTag = constants.PreDeploy;
this.preDeployScripts.length === 0 ? this.preDeployScripts.push(fileEntry) : this.noneDeployScripts.push(fileEntry);
this._preDeployScripts.length === 0 ? this._preDeployScripts.push(fileEntry) : this._noneDeployScripts.push(fileEntry);
break;
case templates.postDeployScript:
xmlTag = constants.PostDeploy;
this.postDeployScripts.length === 0 ? this.postDeployScripts.push(fileEntry) : this.noneDeployScripts.push(fileEntry);
this._postDeployScripts.length === 0 ? this._postDeployScripts.push(fileEntry) : this._noneDeployScripts.push(fileEntry);
break;
default:
xmlTag = constants.Build;
this.files.push(fileEntry);
this._files.push(fileEntry);
}
const attributes = new Map<string, string>();
@@ -345,19 +390,19 @@ export class Project {
}
public async exclude(entry: FileProjectEntry): Promise<void> {
const toExclude: FileProjectEntry[] = this.files.concat(this.preDeployScripts).concat(this.postDeployScripts).concat(this.noneDeployScripts).filter(x => x.fsUri.fsPath.startsWith(entry.fsUri.fsPath));
const toExclude: FileProjectEntry[] = this._files.concat(this._preDeployScripts).concat(this._postDeployScripts).concat(this._noneDeployScripts).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._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));
}
public async deleteFileFolder(entry: FileProjectEntry): Promise<void> {
// 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).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);
const toDeleteFiles: FileProjectEntry[] = this._files.concat(this._preDeployScripts).concat(this._postDeployScripts).concat(this._noneDeployScripts).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.rmdir(x.fsUri.fsPath, { recursive: true })));
@@ -367,7 +412,7 @@ export class Project {
public async deleteDatabaseReference(entry: IDatabaseReferenceProjectEntry): Promise<void> {
await this.removeFromProjFile(entry);
this.databaseReferences = this.databaseReferences.filter(x => x !== entry);
this._databaseReferences = this._databaseReferences.filter(x => x !== entry);
}
/**
@@ -381,7 +426,7 @@ export class Project {
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[];
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
@@ -686,12 +731,12 @@ export class Project {
}
if (!this.databaseReferenceExists(entry)) {
this.databaseReferences.push(entry);
this._databaseReferences.push(entry);
}
}
private databaseReferenceExists(entry: IDatabaseReferenceProjectEntry): boolean {
const found = this.databaseReferences.find(reference => reference.pathForSqlProj() === entry.pathForSqlProj()) !== undefined;
const found = this._databaseReferences.find(reference => reference.pathForSqlProj() === entry.pathForSqlProj()) !== undefined;
return found;
}
@@ -749,7 +794,7 @@ export class Project {
public async addSqlCmdVariableToProjFile(entry: SqlCmdVariableProjectEntry): Promise<void> {
// Remove any entries with the same variable name. It'll be replaced with a new one
if (Object.keys(this.sqlCmdVariables).includes(entry.variableName)) {
if (Object.keys(this._sqlCmdVariables).includes(entry.variableName)) {
await this.removeFromProjFile(entry);
}
@@ -759,7 +804,7 @@ export class Project {
this.findOrCreateItemGroup(constants.SqlCmdVariable).appendChild(sqlCmdVariableNode);
// add to the project's loaded sqlcmd variables
this.sqlCmdVariables[entry.variableName] = <string>entry.defaultValue;
this._sqlCmdVariables[entry.variableName] = <string>entry.defaultValue;
}
private addSqlCmdVariableChildren(sqlCmdVariableNode: any, entry: SqlCmdVariableProjectEntry): void {
@@ -807,7 +852,7 @@ export class Project {
}
else {
this.projFileXmlDoc.documentElement.appendChild(importNode, oldImportNode);
this.importedTargets.push(projectAttributeVal); // Add new import target to the list
this._importedTargets.push(projectAttributeVal); // Add new import target to the list
}
await this.serializeToProjFile(this.projFileXmlDoc);
@@ -866,7 +911,7 @@ export class Project {
}
// 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);
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 });
}
@@ -930,7 +975,7 @@ export class Project {
whiteSpaceAtEndOfSelfclosingTag: true
}); // TODO: replace <any>
await fs.writeFile(this.projectFilePath, xml);
await fs.writeFile(this._projectFilePath, xml);
}
/**
@@ -948,7 +993,7 @@ export class Project {
}
for (let file of list) {
const relativePath = utils.trimChars(utils.trimUri(Uri.file(this.projectFilePath), file), '/');
const relativePath = utils.trimChars(utils.trimUri(Uri.file(this._projectFilePath), file), '/');
if (relativePath.length > 0) {
const fileStat = await fs.stat(file.fsPath);
@@ -974,7 +1019,7 @@ export abstract class ProjectEntry {
}
}
export class FileProjectEntry extends ProjectEntry {
export class FileProjectEntry extends ProjectEntry implements IFileProjectEntry {
/**
* Absolute file system URI
*/

View File

@@ -77,6 +77,13 @@ export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvide
return vscode.Uri.file(projectFile);
}
/**
* Opens and loads a .sqlproj file
*/
openProject(projectFilePath: string): Promise<sqldbproj.ISqlProject> {
return Project.openProject(projectFilePath);
}
/**
* Gets the supported project types
*/
@@ -116,15 +123,6 @@ export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvide
return [group, changeTargetPlatformAction];
}
/** 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<void> {
const project = await Project.openProject(projectFile.fsPath);
await project.addToProject(list);
}
/**
* Gets the data to be displayed in the project dashboard
*/

View File

@@ -23,10 +23,96 @@ declare module 'sqldbproj' {
createProject(name: string, location: vscode.Uri, projectTypeId: string): Promise<vscode.Uri>;
/**
* 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<void>;
* Opens and loads a .sqlproj file
*/
openProject(projectFilePath: string): Promise<ISqlProject>;
}
export interface ISqlProject {
/**
* Reads the project setting and contents from the file
*/
readProjFile(): Promise<void>;
/**
* 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.
*/
addToProject(list: vscode.Uri[]): Promise<void>;
/**
* Adds a folder to the project, and saves the project file
* @param relativeFolderPath Relative path of the folder
*/
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
*/
addScriptItem(relativeFilePath: string, contents?: string, itemType?: string): Promise<IFileProjectEntry>;
/**
* Adds a SQLCMD variable to the project
* @param name name of the variable
* @param defaultValue
*/
addSqlCmdVariable(name: string, defaultValue: string): Promise<void>;
/**
* Excludes entry from project by removing it from the project file
* @param entry
*/
exclude(entry: IFileProjectEntry): Promise<void>;
/**
* Deletes file or folder and removes it from the project file
* @param entry
*/
deleteFileFolder(entry: IFileProjectEntry): Promise<void>;
/**
* returns the sql version the project is targeting
*/
getProjectTargetVersion(): string;
/**
* Path where dacpac is output to after a successful build
*/
readonly dacpacOutputPath: string;
/**
* Path to folder containing the project file
*/
readonly projectFolderPath: string;
/**
* Project file path
*/
readonly projectFilePath: string;
/**
* Project file name
*/
readonly projectFileName: string;
/**
* Files and folders that are included in the project
*/
readonly files: IFileProjectEntry[];
/**
* SqlCmd variables and their values
*/
readonly sqlCmdVariables: Record<string, string>;
}
/**
* Represents an entry in a project file
*/
export interface IFileProjectEntry {
fsUri: vscode.Uri;
relativePath: string;
}
}