@sassoftware/vi-solution-extension-upload
Version:
Uploads controls to a SAS Visual Investigator instance
275 lines (240 loc) • 8.17 kB
JavaScript
// This is used so that we can call https without a certificate
process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0;
const env = process.env;
const cwd = process.cwd();
const fs = require("fs");
const glob = require("glob");
const axios = require("axios");
const packageJson = require(`${cwd}/package.json`);
const NOT_APPLICABLE_DIRECTIVE = "not-applicable";
const NOT_APPLICABLE_LABEL = "Not Applicable";
const solutionName = packageJson.name;
const solutionDisplayName = packageJson.name || packageJson.displayName;
const projectTypes = {
DESKTOP: "elements",
MOBILE: "mobile-elements"
};
const config = {
hostname: env.SVI_HOSTNAME,
username: !env.SVI_USERNAME || env.SVI_USERNAME === "" ? null : env.SVI_USERNAME,
password: !env.SVI_PASSWORD || env.SVI_PASSWORD === "" ? null : env.SVI_PASSWORD,
bundlePath: getBundlePath() || `${cwd}/dist/solution-extensions/main.*.js`,
controlsPath: env.SVI_CONTROLS_PATH,
configPath: `${cwd}/projects/components/src/lib`,
bearerToken: env.SVI_BEARER_TOKEN ?? null
};
async function main() {
require("@babel/register")({
presets: ["@babel/preset-env", "@babel/preset-typescript"],
plugins: ["@babel/plugin-transform-runtime"],
ignore: [/node_modules\/(?!@sassoftware\/vi-api)/],
extensions: [".js", ".ts"]
});
// ignore any react component imports.
// https://github.com/nodejs/node/issues/32483
// eslint-disable-next-line node/no-deprecated-api
require.extensions[".tsx"] = () => {};
const logonToken = config.bearerToken ?? (await getLogonToken());
verifyConfig();
configureAxios(logonToken);
const controlMetadata = getConfigs(solutionName, solutionDisplayName, config.bundlePath);
const controlActionsObj = getActions(solutionDisplayName);
// Upload the controls to configured svi server.
uploadControls(controlMetadata, controlActionsObj).catch(console.log);
}
function verifyConfig() {
if (!config.hostname) {
throw new Error("Set SAS Visual Investigator server host name - environment variable SVI_HOSTNAME");
}
// If bearer token property exists but is empty, throw an error.
if (env.SVI_BEARER_TOKEN === "") {
throw new Error("Set SAS Visual Investigator bearer token - environment variable SVI_BEARER_TOKEN");
}
// If the SVI_BEARER_TOKEN doesn't exist, then the username and password must be set.
if (!env.SVI_BEARER_TOKEN && !config.username) {
throw new Error("Set SAS Visual Investigator admin username - environment variable SVI_USERNAME");
}
if (!env.SVI_BEARER_TOKEN && !config.password) {
throw new Error("Set SAS Visual Investigator admin password - environment variable SVI_PASSWORD");
}
}
function getProjectName() {
const projectIndex = process.argv.indexOf("--project");
const projectValue = projectIndex !== -1 && process.argv[projectIndex + 1];
return projectValue;
}
function getBundlePath() {
const projectName = getProjectName();
if (projectName === projectTypes.MOBILE) {
return env.SMI_BUNDLE_PATH;
} else {
return env.SVI_BUNDLE_PATH;
}
}
function configureAxios(logonToken) {
axios.defaults.headers.common.Authorization = "Bearer " + logonToken;
}
function getSourceBundle(bundleGlob) {
const codeSrc = glob.sync(bundleGlob);
// Only expect a single file to be found, if anything else error.
if (codeSrc.length > 1) {
throw new Error("Source matched > 1 file - does the dist directory need to be cleaned?");
} else if (codeSrc.length === 0) {
throw new Error("No source file found");
}
return fs.readFileSync(codeSrc[0], "utf-8");
}
function getConfigs(key, name, bundleGlob) {
const code = getSourceBundle(bundleGlob);
const isMobile = getProjectName() === projectTypes.MOBILE;
if (isMobile) {
return [
{
controlCategory: "MobileHomepageControls",
controlDescription: name,
directiveName: NOT_APPLICABLE_DIRECTIVE,
displayName: name,
name: `${key}-mobile`,
customControl: true,
propertiesTitle: NOT_APPLICABLE_LABEL,
controlAttributes: {
metadata: { showInToolbox: "false" }
},
directive: code
}
];
} else {
return [
{
controlCategory: "Fields",
controlDescription: name,
directiveName: NOT_APPLICABLE_DIRECTIVE,
displayName: name,
name: key,
customControl: true,
propertiesTitle: NOT_APPLICABLE_LABEL,
controlAttributes: {
metadata: { showInToolbox: "false" }
},
directive: code
}
];
}
}
function getActions() {
const controlActionsObj = {
controls: [],
actions: []
};
let controls = [];
if (config.controlsPath) {
controls = require(`${cwd}/${config.controlsPath}`).default;
} else {
const files = glob.sync(config.configPath + "/**/*.control.ts");
controls = files?.map((f) => require(f).control);
}
controls?.forEach(function (data) {
if (data.category === "ToolbarItems") {
controlActionsObj.controls.push({
controlCategory: data.category,
controlDescription: data.name,
directiveName: data.directiveName,
displayName: data.displayName.defaultText,
name: data.name,
customControl: true,
propertiesTitle: NOT_APPLICABLE_LABEL,
controlAttributes: data.controlAttributes,
directive: ""
});
controlActionsObj.actions.push({
actionName: data.name,
controlName: data.name,
displayName: data.displayName,
clientApplication: "desktop",
requiredCapabilities: [],
customAction: true,
actionAttributes: data.controlAttributes,
solutionName: "sas_visual_investigator_default"
});
}
});
return controlActionsObj;
}
async function uploadControls(controlsArray, controlActionsObj) {
const isMobile = getProjectName() === projectTypes.MOBILE;
console.log("Uploading solution: start");
await axios
.post(
getUrl("/svi-datahub/config"),
JSON.stringify({
controls: [...controlsArray, ...controlActionsObj.controls],
actions: controlActionsObj.actions
}),
{
headers: { "Content-Type": "application/json" }
}
)
.then(async () => {
if (isMobile) {
// Clear the mobile cache after each upload.
await axios
.put(getUrl("/SASMobileInvestigator/cache"), {}, {})
.then(() => {
console.log("Clear mobile cache: success");
})
.catch(() => {
console.error("Clear mobile cache: failure");
});
}
console.log("Uploading solution: done");
})
.catch((err) => {
handleAxiosError(err);
console.error("Error calling svi-datahub");
console.error("Uploading solution: failed");
});
}
function getLogonToken() {
const url = getUrl("/SASLogon/oauth/token");
const logonData = `grant_type=password&username=${config.username}&password=${config.password}`;
const logonOptions = {
headers: {
accept: "application/json",
Authorization: "Basic c2FzLmVjOg=="
}
};
return axios
.post(url, logonData, logonOptions)
.then((tokenResponse) => {
return tokenResponse.data.access_token;
})
.catch((err) => {
console.error("Error Calling SASLogon");
handleAxiosError(err);
});
}
function getUrl(path) {
return config.hostname + path;
}
function handleAxiosError(error) {
if (error.response) {
/*
* The request was made and the server responded with a
* status code that falls out of the range of 2xx
*/
console.log(error.response.data);
console.log(error.response.status);
console.log(error.response.headers);
} else if (error.request) {
/*
* The request was made but no response was received, `error.request`
* is an instance of XMLHttpRequest in the browser and an instance
* of http.ClientRequest in Node.js
*/
console.log(error.request);
} else {
// Something happened in setting up the request and triggered an Error
console.log("Error", error.message);
}
}
exports.upload = main;