mcdev
Version:
Accenture Salesforce Marketing Cloud DevTools
377 lines (352 loc) • 17.6 kB
JavaScript
;
import AttributeGroup from './AttributeGroup.js';
import MetadataType from './MetadataType.js';
import { Util } from '../util/util.js';
import cache from '../util/cache.js';
/**
* @typedef {import('../../types/mcdev.d.js').MetadataTypeItem} MetadataTypeItem
* @typedef {import('../../types/mcdev.d.js').MetadataTypeMap} MetadataTypeMap
* @typedef {import('../../types/mcdev.d.js').MetadataTypeMapObj} MetadataTypeMapObj
*/
/**
* AttributeSet MetadataType
*
* @augments MetadataType
*/
class AttributeSet extends MetadataType {
static systemValueDefinitions;
/**
* Retrieves Metadata of schema set Definitions.
*
* @param {string} retrieveDir Directory where retrieved metadata directory will be saved
* @param {void | string[]} [_] unused parameter
* @param {void | string[]} [__] unused parameter
* @param {string} [key] customer key of single item to retrieve
* @returns {Promise.<MetadataTypeMapObj>} Promise
*/
static async retrieve(retrieveDir, _, __, key) {
if (retrieveDir && !cache.getCache()?.attributeGroup) {
// ! attributeGroup and attributeSet both link to each other. caching attributeGroup here "manually", assuming that it's quicker than the other way round
Util.logger.info(' - Caching dependent Metadata: attributeGroup');
AttributeGroup.buObject = this.buObject;
AttributeGroup.client = this.client;
AttributeGroup.properties = this.properties;
const result = await AttributeGroup.retrieveForCache();
cache.setMetadata('attributeGroup', result.metadata);
}
return super.retrieveREST(retrieveDir, '/hub/v1/contacts/schema/setDefinitions', null, key);
}
/**
* Retrieves Metadata of schema set definitions for caching.
*
* @returns {Promise.<MetadataTypeMapObj>} Promise
*/
static retrieveForCache() {
return super.retrieveREST(null, '/hub/v1/contacts/schema/setDefinitions');
}
/**
* used to identify updated shared data extensions that are used in attributeSets.
* helper for DataExtension.#fixShared_onBU
*
* @param {Object.<string, string>} sharedDataExtensionMap ID-Key relationship of shared data extensions
* @param {object} fixShared_fields DataExtensionField.fixShared_fields
* @returns {Promise.<string[]>} Promise of list of shared dataExtension IDs
*/
static async fixShared_retrieve(sharedDataExtensionMap, fixShared_fields) {
if (!Object.keys(sharedDataExtensionMap).length) {
return [];
}
const result = await super.retrieveREST(null, '/hub/v1/contacts/schema/setDefinitions');
const metadataMap = result?.metadata;
if (metadataMap && Object.keys(metadataMap).length) {
const sharedDeIds = Object.keys(metadataMap)
.filter(
(asKey) =>
metadataMap[asKey].storageLogicalType === 'ExactTargetSchema' ||
metadataMap[asKey].storageLogicalType === 'DataExtension'
)
.filter((asKey) => {
// check if dataExtension ID is found on any attributeSet of this BU
if (sharedDataExtensionMap[metadataMap[asKey].storageReferenceID.value]) {
Util.logger.debug(
` shared dataExtension ID ${metadataMap[asKey].storageReferenceID.value} found in attributeSet ${asKey}`
);
return true;
} else {
return false;
}
})
.filter((asKey) => {
// check if any of the dataExtension fields dont exist on the attributeSet or are out of date
const deKey =
sharedDataExtensionMap[metadataMap[asKey].storageReferenceID.value];
const asFields = metadataMap[asKey].valueDefinitions;
const deFields = Object.values(fixShared_fields[deKey]);
return deFields.some((deField) => {
const search = asFields.filter((asf) => asf.name === deField.Name);
if (!search.length) {
Util.logger.debug(
Util.getGrayMsg(
` - Field ${deField.Name} not found in attributeSet; Note: only first recognized difference is printed to log`
)
);
return true;
}
const asField = search[0];
if (asField.dataType !== deField.FieldType) {
Util.logger.debug(
Util.getGrayMsg(
` - Field ${deField.Name} FieldType changed (old: ${asField.dataType}; new: ${deField.FieldType}); Note: only first recognized difference is printed to log`
)
);
return true;
}
asField.defaultValue ||= '';
if (
(asField.defaultValue && deField.DefaultValue === '') ||
(deField.FieldType === 'Boolean' &&
deField.DefaultValue !== '' &&
(deField.DefaultValue
? 'True'
: 'False' !== asField.defaultValue)) ||
(deField.FieldType !== 'Boolean' &&
deField.DefaultValue !== asField.defaultValue)
) {
Util.logger.debug(
` - Field ${deField.Name} DefaultValue changed (old: ${asField.defaultValue}; new: ${deField.DefaultValue}); Note: only first recognized difference is printed to log`
);
return true;
}
// some field types don't carry the length property. reset to 0 to ease comparison
asField.length ||= 0;
if (asField.length !== deField.MaxLength) {
Util.logger.debug(
` - Field ${deField.Name} MaxLength changed (old: ${asField.length}; new: ${deField.MaxLength}); Note: only first recognized difference is printed to log`
);
return true;
}
if (asField.isNullable !== deField.IsRequired) {
Util.logger.debug(
` - Field ${deField.Name} IsRequired changed (old: ${asField.isNullable}; new: ${deField.IsRequired}); Note: only first recognized difference is printed to log`
);
return true;
}
if (asField.isPrimaryKey !== deField.IsPrimaryKey) {
Util.logger.debug(
` - Field ${deField.Name} IsPrimaryKey changed (old: ${asField.isPrimaryKey}; new: ${deField.IsPrimaryKey}); Note: only first recognized difference is printed to log`
);
return true;
}
return false;
});
})
.map((key) => metadataMap[key].storageReferenceID.value)
.filter(Boolean);
return sharedDeIds;
} else {
// nothing to do - return empty array
return [];
}
}
/**
* Builds map of metadata entries mapped to their keyfields
*
* @param {object} body json of response body
* @param {string} [singleRetrieve] key of single item to filter by
* @returns {MetadataTypeMap} keyField => metadata map
*/
static parseResponseBody(body, singleRetrieve) {
const metadataCache = super.parseResponseBody(body);
// make sure we add the entire list to cache before running postRetrieveTasks because of the self-references this type is using
// usually, the cache is only written into after all postRetrieveTasks have been run
cache.setMetadata(this.definition.type, metadataCache);
const metadataStructure = super.parseResponseBody(body, singleRetrieve);
return metadataStructure;
}
/**
* manages post retrieve steps
*
* @param {MetadataTypeItem} metadata a single metadata
* @returns {MetadataTypeItem} metadata
*/
static postRetrieveTasks(metadata) {
// folder
if (metadata.storageLogicalType === 'DataExtension') {
// attributeSet created for Group Connect do not have a folder
super.setFolderPath(metadata);
}
// source
switch (metadata.storageLogicalType) {
case 'ExactTargetSchema': // synced / shared DEs
case 'DataExtension': {
// shared / local DEs
try {
metadata.r__dataExtension_key = cache.searchForField(
'dataExtension',
metadata.storageReferenceID.value,
'ObjectID',
'CustomerKey'
);
// TODO: check if fields in metadata.sendAttributeStorageName exist in data extension --> error
// TODO: check if fields in data extension exist in metadata.sendAttributeStorageName --> warn
delete metadata.storageReferenceID;
delete metadata.storageName;
delete metadata.storageObjectInformation; // type ExactTargetSchema only
} catch (ex) {
Util.logger.warn(
` - ${this.definition.type} ${metadata[this.definition.keyField]}: ${
ex.message
}`
);
}
break;
}
case 'MobileAttributes': {
// TODO: implement
// "storageName": "_MobileAddress",
break;
}
case 'EnterpriseAttributes': {
// TODO: implement
// "storageName": "_EnterpriseAttribute",
break;
}
case 'PushAttributes': {
// TODO: implement
// "storageName": "_PushAddress",
break;
}
}
// relationships to attributeGroups & AttributeSet
if (Array.isArray(metadata.relationships)) {
for (const relationship of metadata.relationships) {
for (const type of ['left', 'right']) {
if (
relationship[type + 'Item']?.connectingID?.identifierType ===
'FullyQualifiedName'
) {
delete relationship[type + 'Item'].connectingID;
}
let relationshipObj = null;
switch (relationship[type + 'Item'].relationshipType) {
case 'AttributeGroup': {
try {
relationship[type + 'Item'].r__attributeGroup_key =
cache.searchForField(
'attributeGroup',
relationship[type + 'Item']?.identifier,
'definitionID',
'definitionKey'
);
delete relationship[type + 'Item']?.identifier;
} catch (ex) {
Util.logger.warn(
` - ${this.definition.type} ${
metadata[this.definition.keyField]
}: ${ex.message}`
);
}
// get relationship fieldnames
relationshipObj = {
valueDefinitions: this._getSystemValueDefinitions(),
};
break;
}
case 'AttributeSet': {
try {
relationship[type + 'Item'].r__attributeSet_key =
cache.searchForField(
'attributeSet',
relationship[type + 'Item']?.identifier,
'definitionID',
'definitionKey'
);
delete relationship[type + 'Item']?.identifier;
// get relationship fieldnames
// check if its a self-reference to metadata.valueDefinitions or if it's a reference to another attributeSet
relationshipObj =
relationship[type + 'Item'].r__attributeSet_key ===
metadata.definitionKey
? metadata
: cache.getByKey(
'attributeSet',
relationship[type + 'Item'].r__attributeSet_key
);
} catch (ex) {
Util.logger.warn(
` - ${this.definition.type} ${
metadata[this.definition.keyField]
}: ${ex.message}`
);
}
break;
}
}
try {
// get relationship fieldnames
// resolve field values
for (const attr of relationship.relationshipAttributes) {
const id = attr[type + 'AttributeID'];
const valueDefinition = relationshipObj.valueDefinitions.find(
(item) => item.valueDefinitionID === id
);
if (valueDefinition) {
attr['c__' + type + 'FullyQualifiedName'] =
valueDefinition.fullyQualifiedName;
delete attr[type + 'AttributeID'];
delete attr[type + 'ConnectingID'];
} else {
throw new Error(
`Could not find ${type}AttributeID ${id} of relationship ${relationship.relationshipID}`
);
}
}
} catch (ex) {
Util.logger.warn(
` - ${this.definition.type} ${metadata[this.definition.nameField]} / ${
metadata[this.definition.keyField]
}: ${ex.message}`
);
}
}
}
}
// Member ID
delete metadata.customObjectOwnerMID;
// remove duplicate ID fields (main field is definitionID)
delete metadata.setDefinitionID;
if (metadata.dataRetentionProperties?.setDefinitionID) {
delete metadata.dataRetentionProperties?.setDefinitionID;
}
// connectingID.identifierType seems to be always set to 'FullyQualifiedName' - to be sure we check it here and remove it if it's the case
if (metadata.connectingID?.identifierType === 'FullyQualifiedName') {
// remove useless field
delete metadata.connectingID;
}
return metadata;
}
/**
* helper for {@link AttributeSet.postRetrieveTasks}
*
* @returns {object[]} all system value definitions
*/
static _getSystemValueDefinitions() {
this.systemValueDefinitions ||= {};
if (!this.systemValueDefinitions[this.buObject.mid]) {
this.systemValueDefinitions[this.buObject.mid] = Object.values(
cache.getCache()['attributeSet']
)
.flatMap((item) => {
if (item.isSystemDefined) {
return item.valueDefinitions;
}
})
.filter(Boolean);
}
return this.systemValueDefinitions[this.buObject.mid];
}
}
// Assign definition to static attributes
import MetadataTypeDefinitions from '../MetadataTypeDefinitions.js';
AttributeSet.definition = MetadataTypeDefinitions.attributeSet;
export default AttributeSet;