dynamicsmobile
Version:
Allows development of off-line mobile and web business apps over the Dynamics Mobile platform. More info on https://www.dynamicsmobile.com
353 lines (303 loc) • 12.1 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');
var baseAPIUrl = "https://api.portal.dynamicsmobile.com";
var binPath = './.bin/user/apparea/SANDBOX/APP';
let appPackage = null;
async function uploadAppZip(filePath, app, profile) {
return new Promise(async function (resolve, reject) {
const 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;
}
var 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) {
var 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;
}
var 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');
for (let i = 0; i < appPackage.dms.locales.length; i++) {
fs.writeFileSync(path.join(binPath, appPackage.dms.locales[i], 'dms-bo.html'), content);
}
content = fs.readFileSync('./.bin/dms-bo.json', 'utf8');
for (let i = 0; i < appPackage.dms.locales.length; i++) {
fs.writeFileSync(path.join(binPath, appPackage.dms.locales[i], 'dms-bo.json'), content);
}
//generate metadata file for dms agent
const md = JSON.parse(content);
const newMd = {};
newMd[appCode] = md;
for (let i = 0; i < appPackage.dms.locales.length; i++) {
fs.writeFileSync(path.join(binPath, appPackage.dms.locales[i], 'metadata.json'), JSON.stringify(newMd));
}
content = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
var app = {
appCode: content.name.toUpperCase(),
version: content.version,
appType: content.dms.appType,
defaultTask: "Home",
system: (content.dms.system == true ? true : false)
};
for (let i = 0; i < appPackage.dms.locales.length; i++) {
fs.writeFileSync(path.join(binPath, appPackage.dms.locales[i], 'app.json'), JSON.stringify(app));
}
content = fs.readFileSync(path.join(binPath, 'en', 'Home.html'), 'utf8');
for (let i = 0; i < appPackage.dms.locales.length; i++) {
fs.writeFileSync(path.join(binPath, appPackage.dms.locales[i], 'modules.html'), content);
fs.writeFileSync(path.join(binPath, appPackage.dms.locales[i], appCode + '.html'), content);
}
resolve();
}
catch (err) {
reject(err);
}
});
}
function deleteNonNeededFiles(folder) {
return new Promise(function (resolve, reject) {
try {
var files = fs.readdirSync(folder);
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, manifest, appType, user, language) {
return new Promise(async function (resolve, reject) {
try {
var url = baseAPIUrl + `/studio/${appCode}/studio2marketplace?version=${version}&appType=${appType}`
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
},
manifest: manifest
})
};
const response = await fetch(url, options);
if (!response.ok) {
const errorText = await response.text();
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
};
const response = await fetch(url, {
method: "PUT",
headers: headers,
body: fileStream
});
if (!response.ok) {
const errorText = await response.text();
return reject(errorText);
}
resolve();
}
catch (err) {
reject(err);
}
});
}
async function executeAppPublishing() {
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;
}
if (!fs.existsSync('./appmanifest.json')) {
console.log(chalk.red('DMS ERROR: File appmanifest.json does not exists.Can not publish app to marketplace.'));
console.log();
process.exit(1);
return;
}
appPackage = fs.readFileSync('./package.json', 'utf8');
try {
appPackage = JSON.parse(appPackage);
}
catch (err) {
console.log(chalk.red('DMS ERROR: File package.json has invalid format!'));
console.log();
process.exit(1);
return;
}
const app = appPackage;
var manifest = fs.readFileSync('./appmanifest.json', 'utf8');
try {
manifest = JSON.parse(manifest);
}
catch (err) {
console.log(chalk.red('DMS ERROR: File appmanifest.json has invalid format!'));
console.log();
process.exit(1);
return;
}
if (app.dms.appArea != profile.appArea) {
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;
}
await copyOtherFiles(app.name.toUpperCase());
console.log(chalk.blue(`Dynamics Mobile is releasing app to marketplace: ${app.name.toUpperCase()}, v.${app.version} area: ${profile.appArea} ...`));
var localAppZipPath = `./.dms/${app.name.toUpperCase()}.zip`;
var localDemoDataZipPath = `./.dms/${app.name.toUpperCase()}_demoData.zip`;
if (!fs.existsSync('./.dms'))
fs.mkdirSync('./.dms');
if (!appPackage.dms.locales || appPackage.dms.locales.length == 0) {
throw new Error('dms.locales are not defined in package.json');
}
console.log('deleteting non needed files...');
await deleteNonNeededFiles(binPath);
for (let i = 0; i < appPackage.dms.locales.length; i++) {
let locale = appPackage.dms.locales[i];
await deleteNonNeededFiles(path.join(binPath, locale));
}
console.log('Preparing app artefacts...');
await zip(binPath, 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('Releasing app to marketplace...');
let actualVersion = app.version;
await publishApp(app.name.toUpperCase(), actualVersion, profile.token, manifest, app.dms.appType, profile.user, app.dms.language);
console.log(chalk.green(`App ${app.name.toUpperCase()}, v.${actualVersion} was released to market place. Review is pending`));
}
catch (err) {
console.log(chalk.red('ERROR: '), err);
process.exit(1);
return;
}
}
executeAppPublishing();