dynamicsmobile
Version:
Allows development of off-line mobile and web business apps over the Dynamics Mobile platform. More info on https://www.dynamicsmobile.com
391 lines (337 loc) • 14.2 kB
JavaScript
/***
Dynamics Mobile
www.dynamicsmobile.com
2025 All rights reserved
publishing of mobile or backend app
*/
const fetch = require('node-fetch');
const fs = require('fs');
const os = require('os');
const path = require('path');
const chalk = require('chalk');
const { zip } = require('zip-a-folder');
const baseAPIUrl = "https://api.portal.dynamicsmobile.com";
const rootBinPath = './.bin/user/apparea/SANDBOX/APP';
let languages = [];
async function uploadAppZip(filePath, app, profile) {
return new Promise(async function (resolve, reject) {
let url;
if (profile.user)
url = `${baseAPIUrl}/studio/${app.name.toUpperCase()}/appupload`;
else
url = `${baseAPIUrl}/studio/${app.name.toUpperCase()}/appupload2`;
try {
const headers = {
'Authorization': profile.user ? profile.token : undefined,
'x-api-key': profile.user ? undefined : profile.token
};
const response = await fetch(url, {
method: "GET",
headers: headers
});
if (!response.ok) {
console.log(chalk.red(`DMS ERROR 102: Dynamics Mobile Cloud returned error status ${response.status}>> ${await response.text()}. Use "dms login" command from command line, first`));
console.log();
reject();
return;
}
const data = await response.json();
await uploadZip(filePath, data.url);
resolve();
} catch (err) {
console.log(chalk.red("DMS ERROR 101: "), err);
console.log();
reject();
}
});
}
async function uploadDemoDataZip(filePath, app, profile) {
return new Promise(async function (resolve, reject) {
let url;
if (profile.user)
url = `${baseAPIUrl}/studio/${app.name.toUpperCase()}/appupload?demoData=true`;
else
url = `${baseAPIUrl}/studio/${app.name.toUpperCase()}/appupload2?demoData=true`;
try {
const headers = {
'Authorization': profile.user ? profile.token : undefined,
'x-api-key': profile.user ? undefined : profile.token
};
const response = await fetch(url, {
method: "GET",
headers: headers
});
if (!response.ok) {
console.log(chalk.red(`DMS ERROR 102: Dynamics Mobile Cloud returned error status ${response.status}>> ${await response.text()}. Use "dms login" command from command line, first`));
console.log();
reject();
return;
}
const data = await response.json();
await uploadZip(filePath, data.url);
resolve();
} catch (err) {
console.log(chalk.red("DMS ERROR 101: "), err);
console.log();
reject();
}
});
}
function copyOtherFiles(appCode) {
return new Promise(function (resolve, reject) {
try {
//generate app map doc automatically
process.argv.push('--silent')
require('./dms-bo-map');
//copy app map doc to .bin
var content = fs.readFileSync('./.bin/dms-bo.html', 'utf8');
fs.writeFileSync(path.join(rootBinPath, 'dms-bo.html'), content);
if (fs.existsSync('./.bin/settings.json')) {
content = fs.readFileSync('./.bin/settings.json', 'utf8');
fs.writeFileSync(path.join(rootBinPath, 'settings.json'), content);
}
content = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
var app = {
appCode: content.name.toUpperCase(),
version: content.version,
appType: content.dms.appType,
defaultTask: "Home",
languages: languages.map(l => l.name),
system: (content.dms.system == true ? true : false)
};
const dmsBoJsonContent = fs.readFileSync('./.bin/dms-bo.json', 'utf8');
languages.forEach(l => {
fs.writeFileSync(path.join(l.fullPath, 'app.json'), JSON.stringify(app));
content = fs.readFileSync(path.join(l.fullPath, 'Home.html'), 'utf8');
fs.writeFileSync(path.join(l.fullPath, 'modules.html'), content);
fs.writeFileSync(path.join(l.fullPath, appCode + '.html'), content);
fs.writeFileSync(path.join(l.fullPath, 'dms-bo.json'), dmsBoJsonContent);
const localeContent = JSON.parse(fs.readFileSync(path.join('./src/', 'locales', l.name + '.json'), 'utf8'));
if (fs.existsSync(path.join('./ext/', 'locales', l.name + '.json'))) {
const localeContentExt = JSON.parse(fs.readFileSync(path.join('./ext/', 'locales', l.name + '.json'), 'utf8'));
Object.keys(localeContentExt).forEach(k => {
localeContent[k] = localeContentExt[k];
});
}
fs.writeFileSync(path.join(l.fullPath, 'locale.json'), JSON.stringify(localeContent));
});
fs.writeFileSync(path.join(rootBinPath, 'app.json'), JSON.stringify(app));
fs.writeFileSync(path.join(rootBinPath, 'languages.json'), JSON.stringify(languages.map(l => l.name)));
resolve();
}
catch (err) {
reject(err);
}
});
}
function deleteNonNeededFiles(folder) {
return new Promise(function (resolve, reject) {
try {
var files = fs.readdirSync(folder, { recursive: true });
files.forEach(function (file) {
if (file.indexOf('.ts') > 0) {
fs.unlinkSync(path.join(folder, file));
}
});
resolve();
}
catch (err) {
reject(err);
}
});
}
async function publishApp(appCode, version, auth, mode, appType, user, language, isSystem, pushtoall) {
return new Promise(async function (resolve, reject) {
try {
let url;
if (user)
url = baseAPIUrl + `/studio/${appCode}/deploy?mode=${mode}&version=${version}&appType=${appType}&pushToAll=1`;
else
url = baseAPIUrl + `/studio/${appCode}/deploy2?mode=${mode}&version=${version}&appType=${appType}pushToAll=1`;
const headers = {
'Content-Type': 'application/json',
'Authorization': user ? auth : undefined,
'x-api-key': user ? undefined : auth
};
const options = {
method: "POST",
headers: headers,
body: JSON.stringify({
appDef: {
appCode: appCode,
version: version,
appType: appType,
language: language,
system: isSystem
}
}),
timeout: 30000
};
const response = await fetch(url, options);
if (!response.ok) {
const errorText = await response.text();
console.log(chalk.red(`DMS ERROR: Publishing failed with status ${response.status}: ${errorText}`));
return reject(errorText);
}
resolve();
}
catch (err) {
console.log(chalk.red('DMS ERROR: '), err);
resolve();
}
});
}
async function uploadZip(localZipPath, url) {
return new Promise(async function (resolve, reject) {
try {
const stats = fs.statSync(path.resolve(localZipPath));
const fileSizeInBytes = stats.size;
const fileStream = fs.createReadStream(path.resolve(localZipPath));
const headers = {
'Content-Type': 'application/zip',
'Content-Length': fileSizeInBytes.toString()
};
const response = await fetch(url, {
method: "PUT",
headers: headers,
body: fileStream
});
if (!response.ok) {
const errorText = await response.text();
console.log(chalk.red(`DMS ERROR: Upload failed with status ${response.status}: ${errorText}`));
return reject(errorText);
}
resolve();
}
catch (err) {
reject(err);
}
});
}
async function executeAppPublishing() {
let mode = 'deploy';
let skipAppAreaControl = false;
let pushtoall = true;
var commandLineArgs = process.argv;
if (commandLineArgs && commandLineArgs.length > 0 && commandLineArgs.indexOf("sandbox") > 0) {
mode = "sandbox";
}
if (commandLineArgs && commandLineArgs.length > 0 && commandLineArgs.indexOf("skipappareacontrol") > 0) {
skipAppAreaControl = true;
}
// if (commandLineArgs && commandLineArgs.length > 0 && commandLineArgs.indexOf("pushtoall") > 0) {
// pushtoall = true;
// }
if(!fs.existsSync(rootBinPath)) {
fs.mkdirSync(rootBinPath, { recursive: true });
}
var homedir = os.homedir();
var folder = path.join(homedir, '.dms');
var profilePath = path.join(folder, 'profile.cfg');
if (!fs.existsSync(profilePath)) {
console.log('DMS ERROR: Dynamics Mobile profile does not exists. Use "dms login" command from command line, first');
process.exit(1);
return;
}
var profile = fs.readFileSync(profilePath, 'utf8');
try {
profile = JSON.parse(profile);
}
catch (err) {
console.log(chalk.red('DMS ERROR: Dynamics Mobile profile is invalid. Use "dms login" from the command line!'));
console.log();
process.exit(1);
return;
}
if (!fs.existsSync('./package.json')) {
console.log(chalk.red('DMS ERROR: File package.json does not exists. Use "dms init" command from command line, first'));
console.log();
process.exit(1);
return;
}
var app = fs.readFileSync('./package.json', 'utf8');
try {
app = JSON.parse(app);
}
catch (err) {
console.log(chalk.red('DMS ERROR: File package.json has invalid format!'));
console.log();
process.exit(1);
return;
}
if (app.dms.appArea != profile.appArea && !skipAppAreaControl) {
console.log();
console.log(chalk.white.bgRed(' ERROR '), `Wrong Application Area!`);
console.log(`apparea in package.json/dms/apparea`, chalk.bgGreen(` ${app.dms.appArea} `), `is not the same as the current profile apparea`, chalk.bgGreen(` ${profile.appArea} `));
console.log();
process.exit(1);
return;
}
//if (app.dms.appType && app.dms.appType.toUpperCase() == 'B') {
//pushtoall = true;
//}
// if (!app.dms.appType || app.dms.appType.toUpperCase() != 'M') {
// console.log();
// console.log(chalk.white.bgRed(' ERROR '), `Wrong Application Type!`);
// console.log(`appType in package.json/dms/appType`, chalk.bgGreen(` ${app.dms.appType} `), `Please change it to `, chalk.bgGreen(` m `));
// console.log();
// process.exit(1);
// return;
// }
if (!app.dms.locales) {
console.log();
console.log(chalk.white.bgRed(' ERROR '), `Missing dms.locales in package.json!`);
console.log();
process.exit(1);
}
languages = app.dms.locales.map(l => { return { name: l, fullPath: path.join(rootBinPath, l) } });
// languages = fs.readdirSync(rootBinPath,{
// withFileTypes: true,
// recursive: false
// }).filter(d=>fs.statSync(path.join(rootBinPath,d.name)).isDirectory()).map(d=>{return {name: d.name, fullPath: path.join(rootBinPath, d.name)}});
await copyOtherFiles(app.name.toUpperCase());
console.log(chalk.blue(`Dynamics Mobile is initiating publishing of app: ${app.name.toUpperCase()}, v.${app.version} area: ${profile.appArea} ...`));
// if (pushtoall) {
// console.log('PUSH-TO-ALL ENFORCED');
// }
var localAppZipPath = `./.dms/${app.name.toUpperCase()}.zip`;
var localDemoDataZipPath = `./.dms/${app.name.toUpperCase()}_demoData.zip`;
if (!fs.existsSync('./.dms'))
fs.mkdirSync('./.dms');
await deleteNonNeededFiles(rootBinPath);
console.log('Preparing app artefacts...');
await zip(rootBinPath, localAppZipPath);
//upload demo data, if exists
var localDemoDataPath;
if (fs.existsSync('./ext/Data/init.xml'))
localDemoDataPath = './ext/Data';
else
if (fs.existsSync('./src/Data/init.xml'))
localDemoDataPath = './src/Data';
if (fs.existsSync(localDemoDataZipPath))
fs.unlinkSync(localDemoDataZipPath);
if (localDemoDataPath)
await zip(localDemoDataPath, localDemoDataZipPath);
console.log('Starting app artefacts uploading...');
try {
await uploadAppZip(localAppZipPath, app, profile);
if (localDemoDataZipPath && fs.existsSync(localDemoDataZipPath))
await uploadDemoDataZip(localDemoDataZipPath, app, profile);
console.log(`App artefacts uploaded`);
console.log('Publishing app...');
let actualVersion = app.version;
// if (!pushtoall) {
// const d = new Date();
// const buildNo = `${d.getUTCDate()}${d.getUTCMonth()}${d.getUTCHours()}${d.getUTCMinutes()}${d.getUTCSeconds()}`
// actualVersion = app.version + '.' + buildNo;
// }
await publishApp(app.name.toUpperCase(), actualVersion, profile.token, mode, app.dms.appType, profile.user, app.dms.language, app.dms.system, pushtoall);
console.log(chalk.green(`App ${app.name.toUpperCase()}, v.${actualVersion} was published to production`));
}
catch (err) {
console.log(chalk.red('ERROR: '), err);
process.exit(1);
return;
}
}
executeAppPublishing();