dynamicsmobile
Version:
Allows development of off-line mobile and web business apps over the Dynamics Mobile platform. More info on https://www.dynamicsmobile.com
554 lines (479 loc) • 20 kB
JavaScript
/***
Dynamics Mobile
www.dynamicsmobile.com
2025 All rights reserved
*/
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const chalk = require('chalk');
const os = require('os');
const { resolve } = require('path');
const { readdir } = require('fs').promises;
function getDirectoryFilesSync(dir) {
var results = [];
if (fs.existsSync(dir)) {
var list = fs.readdirSync(dir);
list.forEach(function (file) {
file = path.join(dir, file);
var stat = fs.statSync(file);
if (stat && stat.isDirectory()) {
/* Recurse into a subdirectory */
results = results.concat(getDirectoryFilesSync(file));
} else {
/* Is a file */
results.push(file);
}
});
}
return results;
}
function checkAndProcessProfile() {
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;
}
const title = (' ' + profile.appArea + ' ');
console.log(chalk.blue(' -'.padEnd(profile.appArea.length + 9, '-')));
console.log(chalk.blue('| '), chalk.bgCyan.white(title.padEnd(10, ' ').padStart(12, ' ')), chalk.blue('|'));
console.log(chalk.blue(' -'.padEnd(profile.appArea.length + 9, '-')));
}
function getTypescirptEnumNameFromProp(boName, key, prop) {
return `${boName}_${key}_enum`;
}
function getTypescriptTypeFromProp(boName, key, prop) {
if (prop.List)
return getTypescirptEnumNameFromProp(boName, key, prop);
switch (prop.type.toLowerCase()) {
case 'string': return 'string';
case 'int32':
case 'bigint':
case 'int64':
case 'decimal':
case 'double': return 'number';
case 'datetime': return 'Date';
case 'date': return 'Date';
case 'boolean': return 'boolean';
case 'bit': return 'boolean';
case 'binary': return 'string';
case 'ref': return 'string';
default: return 'UNKOWN PROPERTY TYPE:' + prop.type;
}
}
function generate() {
console.log('');
checkAndProcessProfile();
console.log(chalk.white.bgGreen(' DMS '), 'Dynamics Mobile Business Object Generator is working...');
let enforceGeneration = false;
let useSeperateBOFile = false;
let commandLineArgs = process.argv;
if (commandLineArgs && commandLineArgs.length > 0 && commandLineArgs.indexOf("enforce") >= 0) {
enforceGeneration = true;
console.log(chalk.white.bgGreen(' DMS '), chalk.gray('genbo was forced from cmd'));
}
if (commandLineArgs && commandLineArgs.length > 0 && commandLineArgs.indexOf("manybo") >= 0) {
useSeperateBOFile = true;
console.log(chalk.white.bgGreen(' DMS '), chalk.gray('single BO generation was forced'));
}
if (commandLineArgs && commandLineArgs.length > 0 && commandLineArgs.indexOf("clean") >= 0) {
useSeperateBOFile = true;
console.log(chalk.white.bgGreen(' DMS '), chalk.gray('Cleaning folders'));
//clean .tmp
if (fs.existsSync(path.resolve(`./.tmp`))) {
fs.rmSync(path.resolve(`./.tmp`), { recursive: true });
fs.mkdirSync(path.resolve(`./.tmp`));
}
//clean .dms
if (fs.existsSync(path.resolve(`./.dms`))) {
fs.rmSync(path.resolve(`./.dms`), { recursive: true });
fs.mkdirSync(path.resolve(`./.dms`));
}
//clean .bin
if (fs.existsSync(path.resolve(`./.bin/user/apparea/SANDBOX/APP`))) {
fs.rmdirSync(path.resolve(`./.bin/user/apparea/SANDBOX/APP`), { recursive: true });
fs.mkdirSync(path.resolve(`./.bin/user/apparea/SANDBOX/APP`));
}
}
const rootDir = path.resolve("./");//SLA
const boDir = path.resolve(path.join(rootDir, "./src/Business Objects"));
const extDir = path.resolve(path.join(rootDir, "./ext/Business Objects"));
const appDir = path.resolve(path.join(rootDir, "./src/"))
const actualOutputDir = path.resolve("./.tmp");
const binOutputDir = './.bin';
if (!fs.existsSync(binOutputDir)) {
fs.mkdirSync(binOutputDir)
}
if (!fs.existsSync(path.join(rootDir, "./.tmp"))) {
fs.mkdirSync(path.join(rootDir, "./.tmp"));
}
//delete and recreate Tasks directory
if (fs.existsSync(path.join(rootDir, "./.tmp/Tasks"))) {
fs.rmdirSync(path.join(rootDir, "./.tmp/Tasks"), { recursive: true });
}
fs.mkdirSync(path.join(rootDir, "./.tmp/Tasks"));
var app = fs.readFileSync('./package.json', 'utf8');
app = JSON.parse(app);
const isMobileApp = app.dms.appType == 'm';
//fetch all business objects
var boFiles = [];
if (fs.existsSync(boDir))
boFiles = getDirectoryFilesSync(boDir);
//fetch all extenion business objects
var _extBoFiles = [];
if (fs.existsSync(extDir))
_extBoFiles = getDirectoryFilesSync(extDir);
//check if new extended bo exists
_extBoFiles.forEach(
f => {
const extBoName = path.basename(f)
const boFile = boFiles.find((boF) => {
const boName = path.basename(boF);
return boName == extBoName;
})
if (!boFile) {
boFiles.push(f);
}
}
)
var outputTsFileName = `dms-bo.ts`;
var outputJsonFileName = `dms-bo.json`;
var tsFileHeader = `import { DmsApplicationService,RootDIContainer,appAreaServiceName,${isMobileApp ? 'LiveLinkQuery,' : ''}${isMobileApp ? 'MobileLiveLinkQuery,' : ''}${isMobileApp ? 'MobileDbQuery' : 'BackendDbQuery'}, ${isMobileApp ? 'MobileBusinessObjectBase' : 'BackendBusinessObjectBase'}, ContextInstanceLoader } from '@dms';\n`;
var tsFile = tsFileHeader;
var appDataModel = {};
boFiles = boFiles.map(f => f.replace(boDir, '').replace(extDir, '')).filter(f => {
const tokens = f.split('.');
return tokens && tokens.length == 3 && tokens[1] == 'bo' && tokens[2] == 'json'
})
boFiles.forEach(function (boFile) {
var tokens = path.basename(boFile).split('.');
if (tokens.length == 3 && tokens[2] == 'json') {
var boName = tokens[0]; //boFile.replace(/^.*[\\\/]/, '').split('.').slice(0, -1).join('.');
console.log('b ', boName);
var actualBoDir;
if (fs.existsSync(path.join(extDir, boFile)))
actualBoDir = extDir;
else
actualBoDir = boDir;
var bo = JSON.parse(fs.readFileSync(path.join(actualBoDir, boFile), 'utf8'));
appDataModel[boName] = bo[boName];
var bo = appDataModel[boName];
if (bo == null) {
console.log(chalk.white.bgRed(' DMS '), `Business object name in json file ${boName} is different than the file name `);
console.log('');
process.exit(-1);
}
var staticFieldsCode = '\n';
var membersCode = [];
staticFieldsCode += `static readonly appCode = '${app.name.toUpperCase()}';\n`;
staticFieldsCode += `static readonly boName = '${bo.name}';\n`;
staticFieldsCode += `static readonly boSyncName = '${bo.syncName ? bo.syncName : bo.name}';\n`;
staticFieldsCode += `static readonly boTableName = '${bo.tableName ? bo.tableName : app.name.toUpperCase() + '_' + bo.name}';\n`;
var enumCode = '';
var propsCode = '';
var userModifiedDetected = false;
var dateModifiedDetected = false;
var userCreatedDetected = false;
var dateCreatedDetected = false;
for (var key in bo.properties) {
var prop = bo.properties[key];
// if (key == 'DMS_USERMODIFIED') {
// userModifiedDetected = true;
// }
// if (key == 'DMS_USERCREATED') {
// userCreatedDetected = true;
// }
// if (key == 'DMS_DATEMODIFIED') {
// dateModifiedDetected = true;
// }
// if (key == 'DMS_DATECREATED') {
// dateCreatedDetected = true;
// }
if (prop.List) {
let usedIdentifiers = {};
function labelToIdentifier(label) {
let identifier = label;
// remove non-alphanumeric
identifier = identifier.replace(/[^\da-z]/ig, '');
// do not start with digit
identifier = identifier.replace(/^(?=\d|$)/, '_');
// do not repeat a previous one
while (usedIdentifiers[identifier]) identifier += '_';
usedIdentifiers[identifier] = true;
return identifier;
}
enumCode += `export enum ${getTypescirptEnumNameFromProp(bo.name, key, prop)} {${prop.List.map(l => labelToIdentifier(l.Label) + "=" + JSON.stringify(l.Code)).join(',')}}\n`;
}
propsCode += `public ${key}:${getTypescriptTypeFromProp(bo.name, key, prop)};\n`
membersCode.push(`{ name:'${key}', dataType:'${prop.type}',length:${prop.length ? prop.length : 0}, label:'${prop.label ? prop.label : key}', required:${prop.required ? 'true' : 'false'}, isPK:${key == "DMS_ROWID" ? 'true' : 'false'}, syncTag:'${prop.syncTag ? prop.syncTag : key}',dontSend:${prop.dontSend ? 'true' : 'false'},searching:${prop.searching ? 'true' : 'false'},sorting:${prop.sorting ? 'true' : 'false'},uiType:'${prop.uiType ? prop.uiType : 'Default'}',defaultValue:${prop.defaultValue ? JSON.stringify(prop.defaultValue) : 'null'},list:${prop.List ? getTypescirptEnumNameFromProp(bo.name, key, prop) : 'null'}}`);
}
//include fileds from expand paths
if (bo.expand) {
for (const expandName in bo.expand) {
const expandObject = bo.expand[expandName];
var actualBoDir;
//try to find the entity in ext folder
//console.log(`trying to expand entity ${expandName}=>${expandObject.entity}`);
let businesObjectFiles = getDirectoryFilesSync(extDir);
let boFilePath = null;
boFilePath = businesObjectFiles.find(f => f.f.replace(/\\/g, '/').indexOf(`/${expandObject.entity}.bo.json`) > 0);
if (!boFilePath) {
//try to find expanded entity in src folder
businesObjectFiles = getDirectoryFilesSync(boDir);
//console.log('==>'+businesObjectFiles[0])
boFilePath = businesObjectFiles.find(f => f.replace(/\\/g, '/').indexOf(`/${expandObject.entity}.bo.json`) > 0);
}
if (!fs.existsSync(boFilePath)) {
console.log();
console.log(chalk.red(`DMS ERROR: Expanded business object "${expandObject.entity}" for business object "${boName}" does not exists in the repository!`));
console.log();
process.exit(-1)
}
var expansionBo = JSON.parse(fs.readFileSync(boFilePath, 'utf8'));
try {
for (const propName in expansionBo[expandObject.entity].properties) {
propsCode += `public ${expandName}__${propName}:${getTypescriptTypeFromProp(expandObject.entity, propName, expansionBo[expandObject.entity].properties[propName])};\n`
}
}
catch (ex) {
console.log(expandName);
console.log(boFilePath);
throw ex;
}
}
}
bo.properties['DMS_USERCREATED'] = {
"label": "Created by",
"required": true,
"searching": true,
"sorting": true,
"type": "String",
"uiType": "Default",
"length": 50
};
bo.properties['DMS_USERMODIFIED'] = {
"label": "Modified by",
"required": true,
"searching": true,
"sorting": true,
"type": "String",
"uiType": "Default",
"length": 50
};
bo.properties['DMS_DATECREATED'] = {
"label": "Created on",
"required": true,
"searching": true,
"sorting": true,
"type": "DateTime",
"uiType": "Default",
};
bo.properties['DMS_DATEMODIFIED'] = {
"label": "Modified on",
"required": true,
"searching": true,
"sorting": true,
"type": "DateTime",
"uiType": "Default",
};
const singleBoDef = `
//--- Auto Generated Business Object Deifnition:${bo.name} ---
${enumCode}
export class ${bo.name} extends ${isMobileApp ? 'MobileBusinessObjectBase' : 'BackendBusinessObjectBase'} {
${staticFieldsCode}
public static readonly members:Array<any>=[${membersCode.join(',')}];
${propsCode}
constructor(){
super();
this.members = ${bo.name}.members;
this.appCode = ${bo.name}.appCode;
this.boName = ${bo.name}.boName;
this.boSyncName = ${bo.name}.boSyncName;
this.boTableName = ${bo.name}.boTableName;
}
protected query(): ${isMobileApp ? 'MobileDbQuery<' + bo.name + '>' : 'BackendDbQuery<' + bo.name + '>'} {
return new ${isMobileApp ? 'MobileDbQuery<' + bo.name + '>' : 'BackendDbQuery<' + bo.name + '>'}(${isMobileApp ? bo.name + '.boName' : 'RootDIContainer.inject(DmsApplicationService)'},${isMobileApp ? bo.name + '.boTableName' : `appAreaServiceName, ${bo.name}.appCode,${bo.name}.name, ${bo.name}.boTableName,${bo.name}.boSyncName, ${bo.name}.members`});
}
${isMobileApp ?
`public static livelinkQuery(serviceName?: string, appCode?: string, isSandBox?:boolean):LiveLinkQuery<${bo.name}> {
return new MobileLiveLinkQuery<${bo.name}>(RootDIContainer.inject(DmsApplicationService),serviceName?serviceName:'$$apparea',appCode?appCode:${bo.name}.appCode,${bo.name}.boName,${bo.name}.boTableName, ${bo.name}.boSyncName, ${bo.name}.members,null,isSandBox)
}`: ''}
public static query(): ${isMobileApp ? 'MobileDbQuery<' + bo.name + '>' : 'BackendDbQuery<' + bo.name + '>'} {
return new ${isMobileApp ? 'MobileDbQuery<' + bo.name + '>' : 'BackendDbQuery<' + bo.name + '>'}(${isMobileApp ? bo.name + '.boName' : 'RootDIContainer.inject(DmsApplicationService)'},${isMobileApp ? bo.name + '.boTableName' : `appAreaServiceName, ${bo.name}.appCode,${bo.name}.name, ${bo.name}.boTableName,${bo.name}.boSyncName, ${bo.name}.members`});
}
}
ContextInstanceLoader.regInstance(${bo.name});
`;
if (!useSeperateBOFile) {
tsFile += singleBoDef;
}
else {
const singleBoTsFilePath1 = path.join(actualOutputDir, `${bo.name}.bo.ts`);
const singleBoTsFilePath2 = path.join(binOutputDir, `${bo.name}.bo.ts`);
fs.writeFileSync(singleBoTsFilePath1, tsFileHeader + singleBoDef, 'utf8');
fs.writeFileSync(singleBoTsFilePath2, tsFileHeader + singleBoDef, 'utf8');
}
}
});
//tasks identifiers
var taskTypeScriptCodeArray = [];
var tasksDir = path.resolve(path.join(rootDir, "./src/Tasks"));
var extTasksDir = path.resolve(path.join(rootDir, "./ext/Tasks"));
//write temp tasks based on root meni prefixes
const rootMenuFile = path.resolve(path.join(rootDir, "./src/root-menu.json"));
let rootMenuContent = '';
if (fs.existsSync(rootMenuFile)) {
const rootMenuFile = path.resolve(path.join(rootDir, "./src/root-menu.json"));
const extRootMenuFile = path.resolve(path.join(rootDir, "./ext/root-menu.json"));
if (fs.existsSync(rootMenuFile)) {
const s = fs.readFileSync(rootMenuFile, 'utf8');
rootMenuContent += s
}
if (fs.existsSync(extRootMenuFile)) {
const s = fs.readFileSync(extRootMenuFile, 'utf8');
rootMenuContent += s;
}
const rootMenu = JSON.parse(fs.readFileSync(rootMenuFile, 'utf8'));
let taskIndex = 0;
const processMenuItem = function (menuItem) {
if (menuItem.items && menuItem.items.length > 0) {
for (let ti = 0; ti < menuItem.items.length; ti++) {
processMenuItem(menuItem.items[ti]);
}
}
else {
if (menuItem.taskId && menuItem.taskPrefix) {
//generate task
const taskFileName = `${menuItem.taskId}.task.json`;
//load task from src or ext
let taskContent;
let actualTaskDir;
if (fs.existsSync(path.join(extTasksDir, taskFileName))) {
actualTaskDir = extTasksDir
}
else
if (fs.existsSync(path.join(tasksDir, taskFileName))) {
actualTaskDir = tasksDir
}
else {
throw `Root-menu.json error: Task ${taskFileName} does not exists in src or ext tasks folder!`
}
try {
taskContent = JSON.parse(fs.readFileSync(path.join(actualTaskDir, taskFileName), 'utf8'));
taskContent.id = menuItem.taskPrefix + "_" + taskContent.id;
//save task to tmp
const taskFilePath = path.join(rootDir, "./.tmp/Tasks", `${menuItem.taskPrefix}_${taskFileName}`);
fs.writeFileSync(taskFilePath, JSON.stringify(taskContent), 'utf8');
}
catch (err) {
throw `Root-menu.json error: Task ${taskFileName} has invalid json content!>${err}`
}
}
}
}
rootMenu.items.forEach((item) => {
processMenuItem(item);
})
}
var tmpTasksDir = path.resolve(path.join(rootDir, "./.tmp/Tasks"));
var tmpTasksFiles = fs.readdirSync(tmpTasksDir);
var taskFiles = fs.readdirSync(tasksDir);
taskFiles.push(...tmpTasksFiles);
taskFiles.forEach(function (f) {
var p = f.indexOf('.task.json');
if (p > 0 && p == f.length - '.task.json'.length) {
var taskContent;
var actualTaskDir;
if (fs.existsSync(path.join(extTasksDir, f)))
actualTaskDir = extTasksDir
else
if (fs.existsSync(path.join(tasksDir, f)))
actualTaskDir = tasksDir
else
if (fs.existsSync(path.join(tmpTasksDir, f)))
actualTaskDir = tmpTasksDir
else
throw `Task ${f} does not exists in src or ext tasks folder!`
try {
taskContent = JSON.parse(fs.readFileSync(path.join(actualTaskDir, f), 'utf8'));
}
catch (err) {
throw `Task ${f} has invalid json content!>${err}`
}
var stepsArray = [];
taskContent.steps.forEach(step => {
var stepRouteArr = [];
step.routes.forEach(route => {
stepRouteArr.push(`${route.id}: {id: '${route.id}',target: '${route.target}', validate: ${(route.validate == true ? 'true' : 'false')}, preserveContext: ${(route.preserveContext == true ? 'true' : 'false')}}`);
});
stepsArray.push(`${step.id}:{id:'${step.id}', routes:{${stepRouteArr.join(',')}}}`);
});
taskTypeScriptCodeArray.push(`${taskContent.id}:{ id:'${taskContent.id}', steps:{${stepsArray.join(',')}} }`);
}
});
tsFile += `
//Task deinfitions
export const Tasks = {
${taskTypeScriptCodeArray.join(',')}
}`
//settings
const settingsFile = path.resolve(path.join(rootDir, "./src/settings.json"));
const extSettingsFile = path.resolve(path.join(rootDir, "./ext/settings.json"));
console.log(settingsFile)
var settings, exSettings;
if (fs.existsSync(settingsFile))
settings = JSON.parse(fs.readFileSync(settingsFile));
else
settings = {};
if (fs.existsSync(extSettingsFile))
exSettings = JSON.parse(fs.readFileSync(extSettingsFile));
else
exSettings = {};
//combine settings and extSettings
const keys = Object.getOwnPropertyNames(settings);
keys.forEach(key => {
if (settings[key] && !exSettings[key])
exSettings[key] = settings[key]
})
const allKeys = Object.getOwnPropertyNames(exSettings);
let settingsCode = ''
if (allKeys.length > 0) {
settingsCode += ` \n export enum ConfigurationSettings { ${allKeys.map(k => {
return `${k.replace(/\$/g, '').replace(/\./g, '_')}='${k}'`
}).join(',')} } \n`;
}
tsFile += settingsCode;
//write output
if (!fs.existsSync(actualOutputDir)) {
fs.mkdirSync(actualOutputDir)
}
fs.writeFileSync(path.join(actualOutputDir, outputTsFileName), tsFile, 'utf8');
fs.writeFileSync(path.join(actualOutputDir, outputJsonFileName), JSON.stringify(appDataModel), 'utf8');
const newHash = crypto.createHash('sha256').update(JSON.stringify(appDataModel) + JSON.stringify(exSettings) + taskTypeScriptCodeArray.join(',') + rootMenuContent + JSON.stringify(app)).digest('hex');
let oldHash = '';
if (fs.existsSync(path.resolve(actualOutputDir, 'bohash.dms'))) {
oldHash = fs.readFileSync(path.resolve(actualOutputDir, 'bohash.dms'), 'utf8');
}
if (newHash != oldHash || enforceGeneration) {
fs.copyFileSync(path.join(actualOutputDir, outputTsFileName), path.join(binOutputDir, outputTsFileName));
fs.copyFileSync(path.join(actualOutputDir, outputJsonFileName), path.join(binOutputDir, outputJsonFileName));
fs.unlinkSync(path.join(actualOutputDir, outputTsFileName));
fs.unlinkSync(path.join(actualOutputDir, outputJsonFileName));
fs.writeFileSync(path.resolve(actualOutputDir, 'bohash.dms'), newHash, 'utf8');
}
console.log(chalk.white.bgGreen(' DMS '), `Dynamics Mobile Business Object Generator generated ${boFiles.length} objects`);
}
generate();