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

554 lines (479 loc) 20 kB
/*** 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();