UNPKG

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
/*** 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();