mcdev
Version:
Accenture Salesforce Marketing Cloud DevTools
203 lines (192 loc) • 8.35 kB
JavaScript
import { Util } from './util.js';
import File from './file.js';
import SDK from 'sfmc-sdk';
import Conf from 'conf';
/**
* @typedef {import('../../types/mcdev.d.js').AuthObject} AuthObject
* @typedef {import('../../types/mcdev.d.js').BuObject} BuObject
* @typedef {import('../../types/mcdev.d.js').CodeExtract} CodeExtract
* @typedef {import('../../types/mcdev.d.js').CodeExtractItem} CodeExtractItem
* @typedef {import('../../types/mcdev.d.js').DeltaPkgItem} DeltaPkgItem
* @typedef {import('../../types/mcdev.d.js').Mcdevrc} Mcdevrc
* @typedef {import('../../types/mcdev.d.js').MetadataTypeItem} MetadataTypeItem
* @typedef {import('../../types/mcdev.d.js').MetadataTypeItemDiff} MetadataTypeItemDiff
* @typedef {import('../../types/mcdev.d.js').MetadataTypeItemObj} MetadataTypeItemObj
* @typedef {import('../../types/mcdev.d.js').MetadataTypeMap} MetadataTypeMap
* @typedef {import('../../types/mcdev.d.js').MetadataTypeMapObj} MetadataTypeMapObj
* @typedef {import('../../types/mcdev.d.js').MultiMetadataTypeList} MultiMetadataTypeList
* @typedef {import('../../types/mcdev.d.js').MultiMetadataTypeMap} MultiMetadataTypeMap
* @typedef {import('../../types/mcdev.d.js').SoapRequestParams} SoapRequestParams
* @typedef {import('../../types/mcdev.d.js').TemplateMap} TemplateMap
* @typedef {import('../../types/mcdev.d.js').TypeKeyCombo} TypeKeyCombo
*/
const credentialStore = new Conf({
projectName: 'mcdev',
configName: 'sessions',
clearInvalidConfig: true,
});
const initializedSDKs = {};
let authfile;
const Auth = {
/**
* For each business unit, set up base credentials to be used.
*
* @param {AuthObject} authObject details for
* @param {string} credential of the instance
* @returns {Promise.<void>} -
*/
async saveCredential(authObject, credential) {
const sdk = setupSDK(credential, authObject);
try {
// check credentials to allow clear log output and stop execution
const test = await sdk.auth.getAccessToken();
if (test.error) {
throw new Error(test.error_description);
} else if (test.scope) {
// find missing rights
const missingAccess = sdk.auth
.getSupportedScopes()
.filter((element) => !test.scope.includes(element));
if (missingAccess.length) {
Util.logger.warn(
'Installed package has insufficient access. You might encounter malfunctions!'
);
Util.logger.warn('Missing scope: ' + missingAccess.join(', '));
}
const existingAuth = (await File.pathExists(Util.authFileName))
? await File.readJSON(Util.authFileName)
: {};
existingAuth[credential] = authObject;
await File.writeJSONToFile('./', Util.authFileName.split('.json')[0], existingAuth);
authfile = existingAuth;
}
} catch (ex) {
throw new Error(ex.message);
}
},
/**
* Returns an SDK instance to be used for API calls
*
* @param {BuObject} buObject information about current context
* @returns {SDK} auth object
*/
getSDK(buObject) {
const credentialKey = `${buObject.credential}/${buObject.businessUnit}`;
if (initializedSDKs[credentialKey]) {
// return initialied SDK if available
return initializedSDKs[credentialKey];
} else {
// check existing credentials cached
authfile ||= File.readJsonSync(Util.authFileName);
const newAuthObj = authfile[buObject.credential];
// use client_id + MID to ensure a unique combination across instances
const sessionKey = newAuthObj.client_id + '|' + buObject.mid;
const existingAuthObj = credentialStore.get(sessionKey);
if (!existingAuthObj) {
newAuthObj.account_id = buObject.mid;
}
initializedSDKs[credentialKey] = setupSDK(credentialKey, existingAuthObj || newAuthObj);
return initializedSDKs[credentialKey];
}
},
/**
* helper to clear all auth sessions
*
* @returns {void}
*/
clearSessions() {
credentialStore.clear();
Util.logger.info(`Auth sessions cleared`);
},
};
/**
* Returns an SDK instance to be used for API calls
*
* @param {string} sessionKey key for specific BU
* @param {AuthObject} authObject credentials for specific BU
* @returns {SDK} auth object
*/
function setupSDK(sessionKey, authObject) {
return new SDK(authObject, {
eventHandlers: {
onLoop: (type, accumulator) => {
Util.logger.info(
Util.getGrayMsg(
` - Requesting next batch (currently ${accumulator?.length} records)`
)
);
},
onRefresh: (authObject) => {
authObject.scope = authObject.scope.split(' '); // Scope is usually not an array, but we enforce conversion here for simplicity
credentialStore.set(sessionKey, authObject);
},
onConnectionError: (ex, remainingAttempts) => {
Util.logger.info(
` - Connection problem (Code: ${ex.code}). Retrying ${remainingAttempts} time${
remainingAttempts > 1 ? 's' : ''
}${
ex.endpoint
? Util.getGrayMsg(
' - ' + ex.endpoint.split('rest.marketingcloudapis.com')[1]
)
: ''
}`
);
Util.logger.errorStack(ex);
},
logRequest: (req) => {
const msg = structuredClone(req);
if (msg.url === '/Service.asmx') {
msg.data = msg.data.replaceAll(
/<fueloauth(.*)<\/fueloauth>/gim,
'<fueloauth>*** TOKEN REMOVED ***</fueloauth>'
);
} else if (msg.headers?.Authorization) {
msg.headers.Authorization = 'Bearer *** TOKEN REMOVED ***';
}
switch (Util.OPTIONS.api) {
case 'cli': {
/* eslint-disable no-console */
console.log(
`${Util.color.fgMagenta}API REQUEST >>${Util.color.reset}`,
msg
);
/* eslint-enable no-console */
break;
}
case 'log': {
let data;
if (msg.data) {
data = msg.data;
delete msg.data;
}
Util.logger.debug('API REQUEST >> ' + JSON.stringify(msg, null, 2));
if (data) {
// printing it separately leads to better formatting
Util.logger.debug(
'API REQUEST body >> \n ' +
(typeof data === 'string'
? data
: JSON.stringify(data, null, 2))
);
}
break;
}
// No default
}
},
logResponse: (res) => {
const msg =
typeof res.data == 'string' ? res.data : JSON.stringify(res.data, null, 2);
if (Util.OPTIONS.api === 'cli') {
console.log(`${Util.color.fgMagenta}API RESPONSE <<${Util.color.reset}`, msg); // eslint-disable-line no-console
} else if (Util.OPTIONS.api === 'log') {
Util.logger.debug('API RESPONSE body << \n ' + msg);
}
},
},
requestAttempts: 4,
retryOnConnectionError: true,
});
}
export default Auth;