mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-14 17:22:20 -05:00
Add sqlproj property to trace the origin of the project. (#18670)
* Add sqlproj property to trace the origin of the project. As part of the database migration process (schema conversion, in particular) we want to be able to tell when converted schemas are being built/deployed to the actual database server. Given that we rely on the SQL Database Projects ADS extension for the compilation/deployment, we don't have too many options other than updating the said extension. The suggested approach is to make the following changes: 1) Add new property to the sqlproj file (called `DatabaseSource`), which will maintain the origin(s) of the project. The property can contain multiple values (separated by semicolon), in case same project contains objects produced by multiple sources (extract schema, convert from another database, etc.). 2) During build and deploy actions, send the well-known values from the newly added property to the telemetry. We don't want to send any random value of the property, as it may raise some privacy concerns. Instead we define a list of the well-known values that we know do not carry any personal information and send those, if they are specified. This change adds all necessary APIs to the SQl Database projects extension which will be consumed by our migration extensions to populate new `DatabaseSource` property. * Use `undefined` instead of `null` Co-authored-by: Kim Santiago <kisantia@microsoft.com>
This commit is contained in:
@@ -1016,6 +1016,35 @@ export class Project implements ISqlProject {
|
||||
await this.addToProjFile(sqlCmdVariableEntry);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<void> {
|
||||
return this.addValueToCollectionProjectProperty(constants.DatabaseSource, databaseSource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes database source from the DatabaseSource property element.
|
||||
* If no sources remain, then property element will be removed from the project file.
|
||||
*
|
||||
* @param databaseSource Source of the database to remove
|
||||
*/
|
||||
public removeDatabaseSource(databaseSource: string): Promise<void> {
|
||||
return this.removeValueFromCollectionProjectProperty(constants.DatabaseSource, databaseSource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an array of all database sources specified in the project.
|
||||
*
|
||||
* @returns Array of all database sources
|
||||
*/
|
||||
public getDatabaseSourceValues(): string[] {
|
||||
return this.getCollectionProjectPropertyValue(constants.DatabaseSource);
|
||||
}
|
||||
|
||||
public createFileProjectEntry(relativePath: string, entryType: EntryType, sqlObjectType?: string): FileProjectEntry {
|
||||
let platformSafeRelativePath = utils.getPlatformSafeFileEntryPath(relativePath);
|
||||
return new FileProjectEntry(
|
||||
@@ -1625,6 +1654,73 @@ export class Project implements ISqlProject {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<void> {
|
||||
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<void> {
|
||||
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.
|
||||
*
|
||||
@@ -1665,12 +1761,116 @@ export class Project implements ISqlProject {
|
||||
const firstPropertyElement = propertyElements[0];
|
||||
if (firstPropertyElement.childNodes.length !== 1) {
|
||||
// Property items are expected to have simple string content
|
||||
throw new Error(constants.invalidProjectPropertyValue(propertyName));
|
||||
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<void> {
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user