├── .gitignore ├── .prettierrc ├── BPMN Server v2.postman_collection.json ├── CHANGELOG.md ├── README.md ├── package-lock.json ├── package.json ├── package.json.bak ├── src ├── API │ ├── API.ts │ ├── AccessManager.ts │ ├── SecureUser.ts │ └── index.ts ├── common │ ├── DefaultConfiguration.ts │ ├── Logger.ts │ ├── index.ts │ └── timer.ts ├── datastore │ ├── Aggregate.ts │ ├── DataStore.ts │ ├── InstanceLocker.ts │ ├── ModelsData.ts │ ├── ModelsDatastore.ts │ ├── ModelsDatastoreDB.ts │ ├── MongoDB.ts │ ├── QueryTranslator.ts │ └── index.ts ├── dmn │ ├── DMNEngine.ts │ └── DMNParser.ts ├── elements │ ├── Definition.ts │ ├── Element.ts │ ├── Events.ts │ ├── Flow.ts │ ├── Gateway.ts │ ├── Node.ts │ ├── NodeLoader.ts │ ├── Process.ts │ ├── Tasks.ts │ ├── Transaction.ts │ ├── behaviours │ │ ├── Behaviour.ts │ │ ├── BehaviourLoader.ts │ │ ├── Error.ts │ │ ├── Escalation.ts │ │ ├── Form.ts │ │ ├── IOBehaviour.ts │ │ ├── Loop.ts │ │ ├── MessageSignal.ts │ │ ├── Script.ts │ │ ├── Terminate.ts │ │ ├── Timer.ts │ │ ├── TransEvents.ts │ │ └── index.ts │ ├── index.ts │ └── js-bpmn-moddle.ts ├── engine │ ├── DataHandler.ts │ ├── DefaultAppDelegate.ts │ ├── Execution.ts │ ├── Item.ts │ ├── Loop.ts │ ├── Model.ts │ ├── ScriptHandler.ts │ ├── Token.ts │ └── index.ts ├── index.ts ├── interfaces │ ├── DataObjects.ts │ ├── DataObjects.ts.bak │ ├── Enums.ts │ ├── User.ts │ ├── common.ts │ ├── datastore.ts │ ├── elements.ts │ ├── engine.ts │ ├── index.ts │ └── server.ts ├── scripts │ ├── convertTomd.js │ ├── copy-readme.ts │ ├── example.ts │ ├── generate-toc.js │ └── tsToApi.ts └── server │ ├── BPMNServer.ts │ ├── CacheManager.ts │ ├── Cron.ts │ ├── Engine.ts │ ├── Listener.ts │ ├── ServerComponent.ts │ └── index.ts ├── tsconfig.json ├── tslint.json └── typedoc.json /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | /node_modules 3 | /obj 4 | /site 5 | *.log 6 | *.cmd 7 | *.sln 8 | *.njsproj 9 | *.njsproj.user 10 | *.txt 11 | *.bak 12 | tsconfig.tsbuildinfo 13 | /bin 14 | *.tgz 15 | *.bak 16 | # Transpiled JavaScript files from Typescript 17 | /dist 18 | # generated docs 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "all", 4 | "singleQuote": true 5 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bpmn-server", 3 | "version": "2.3.5", 4 | "description": "BPMN 2.0 Server including Modeling, Execution and Presistence, an open source for Node.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "tsc --build", 8 | "clean": "rimraf dist", 9 | "docs:api": "rimraf ../docs/docs/api && typedoc --options typedoc.json" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https:github.com/bpmnServer/bpmn-server.git" 14 | }, 15 | "keywords": [ 16 | "BPMN", 17 | "BPMN 2.0", 18 | "Workflow", 19 | "Node.js", 20 | "TypeScript" 21 | ], 22 | "author": { 23 | "name": "ralphhanna" 24 | }, 25 | "main": "dist/index.js", 26 | "types": "dist/index.d.ts", 27 | "files": [ 28 | "src/**/*.ts", 29 | "dist/**/*.js", 30 | "dist/**/*.js.map", 31 | "dist/**/*.d.ts", 32 | "README.md" 33 | ], 34 | "dependencies": { 35 | "@lukeed/uuid": "^2.0.1", 36 | "bcrypt": "^5.0.1", 37 | "body-parser": "^1.20.3", 38 | "bpmn-moddle": "^8.1.0", 39 | "camunda-bpmn-moddle": "^4.4.0", 40 | "chai": "^4.2.0", 41 | "chalk": "^2.4.2", 42 | "compression": "^1.7.4", 43 | "connect-busboy": "^1.0.0", 44 | "connect-flash": "^0.1.1", 45 | "cookie-parser": "^1.4.5", 46 | "core-js": "^3.6.5", 47 | "cron-parser": "^2.16.3", 48 | "dayjs": "^1.11.10", 49 | "debug": "^4.3.1", 50 | "dotenv": "^8.2.0", 51 | "errorhandler": "^1.5.1", 52 | "eventemitter3": "^5.0.1", 53 | "feelin": "^4.3.0", 54 | "fs-extra": "^9.1.0", 55 | "iso8601-duration": "^1.2.0", 56 | "lodash": "^4.17.20", 57 | "lusca": "^1.7.0", 58 | "minimist": "^1.2.8", 59 | "mocha": "^10.2.0", 60 | "mocha-cakes-2": "^3.3.0", 61 | "moment": "^2.29.4", 62 | "mongodb": "^3.6.0", 63 | "morgan": "^1.10.0", 64 | "multer": "*", 65 | "nock": "^12.0.3", 66 | "nodemon": "^3.0.1", 67 | "typedoc-plugin-markdown": "^3.17.1", 68 | "xml2js": "^0.6.2" 69 | }, 70 | "devDependencies": { 71 | "@types/express": "^4.17.7", 72 | "@types/express-serve-static-core": "^4.17.9", 73 | "@types/mime": "^1.3.1", 74 | "@types/node": "^12.12.7", 75 | "@types/serve-static": "^1.13.5", 76 | "prettier": "^2.0.5", 77 | "prism-react-renderer": "^2.3.1", 78 | "react": "^18.2.0", 79 | "react-dom": "^18.2.0", 80 | "rimraf": "^5.0.5", 81 | "ts-node": "^10.9.2", 82 | "tslint": "^6.1.3", 83 | "tslint-config-prettier": "^1.18.0", 84 | "typescript": "^5.3.3" 85 | }, 86 | "engines": { 87 | "node": ">=12.0.0" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /package.json.bak: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bpmn-server", 3 | <<<<<<< HEAD 4 | "version": "1.4.6", 5 | ======= 6 | "version": "2.0.11", 7 | >>>>>>> rel2.0.0 8 | "description": "BPMN 2.0 Server including Modeling, Execution and Presistence, an open source for Node.js", 9 | "license": "MIT", 10 | "scripts": { 11 | "build": "tsc --build", 12 | "clean": "rimraf dist", 13 | "toc": "node src/scripts/generate-toc docs/examples.md,docs/scripting.md,docs/data.md", 14 | "docs:build": "docusaurus build", 15 | "docs:start": "docusaurus start --port 5000", 16 | "docs:generate": "npm run _docs:generate:copy && npm run _docs:generate:api", 17 | "_docs:generate:api": "rimraf docs/api && typedoc --options typedoc.json", 18 | "_docs:generate:copy": "ts-node src/scripts/copy-readme.ts" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https:github.com/bpmnServer/bpmn-server.git" 23 | }, 24 | "keywords": [ 25 | "BPMN", 26 | "BPMN 2.0", 27 | "Workflow", 28 | "Node.js", 29 | "TypeScript" 30 | ], 31 | "author": { 32 | "name": "ralphhanna" 33 | }, 34 | "main": "dist/index.js", 35 | "types": "dist/index.d.ts", 36 | "files": [ 37 | "src/**/*.ts", 38 | "dist/**/*.js", 39 | "dist/**/*.js.map", 40 | "dist/**/*.d.ts", 41 | "README.md" 42 | ], 43 | "dependencies": { 44 | "@docusaurus/theme-search-algolia": "^3.1.0", 45 | "bcrypt": "^5.0.1", 46 | "body-parser": "^1.19.0", 47 | "bpmn-moddle": "^7.0.2", 48 | "camunda-bpmn-moddle": "^4.4.0", 49 | "chai": "^4.2.0", 50 | "chalk": "^2.4.2", 51 | "compression": "^1.7.4", 52 | "connect-busboy": "^1.0.0", 53 | "connect-flash": "^0.1.1", 54 | "cookie-parser": "^1.4.5", 55 | "core-js": "^3.6.5", 56 | "cron-parser": "^2.16.3", 57 | "dayjs": "^1.11.10", 58 | "debug": "^4.3.1", 59 | "dotenv": "^8.2.0", 60 | "errorhandler": "^1.5.1", 61 | "fs-extra": "^9.1.0", 62 | "iso8601-duration": "^1.2.0", 63 | "lodash": "^4.17.20", 64 | "lusca": "^1.7.0", 65 | "minimist": "^1.2.8", 66 | "mocha": "^10.2.0", 67 | "mocha-cakes-2": "^3.3.0", 68 | "moment": "^2.29.4", 69 | "mongodb": "^3.6.0", 70 | "morgan": "^1.10.0", 71 | "multer": "*", 72 | "nock": "^12.0.3", 73 | "nodemon": "^3.0.1", 74 | "uuid": "^8.3.0" 75 | }, 76 | "devDependencies": { 77 | "@docusaurus/core": "^3.0.1", 78 | "@docusaurus/preset-classic": "^3.0.1", 79 | "@docusaurus/types": "^3.0.1", 80 | "@types/express": "^4.17.7", 81 | "@types/express-serve-static-core": "^4.17.9", 82 | "@types/mime": "^1.3.1", 83 | "@types/node": "^12.12.7", 84 | "@types/serve-static": "^1.13.5", 85 | "markdown-toc": "^1.2.0", 86 | "prettier": "^2.0.5", 87 | "prism-react-renderer": "^2.3.1", 88 | "react": "^18.2.0", 89 | "react-dom": "^18.2.0", 90 | "rimraf": "^5.0.5", 91 | "ts-node": "^10.9.2", 92 | "tslint": "^6.1.3", 93 | "tslint-config-prettier": "^1.18.0", 94 | "typedoc": "^0.25.4", 95 | "typedoc-plugin-markdown": "^3.17.1", 96 | "typescript": "^5.3.3" 97 | }, 98 | "engines": { 99 | "node": ">=12.0.0" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/API/AccessManager.ts: -------------------------------------------------------------------------------- 1 | import { ISecureUser, IUserInfo } from "../interfaces"; 2 | 3 | enum USER_ROLE { 4 | SYSTEM = 'SYSTEM', // SYSTEM ADMIN 5 | ADMIN = 'ADMIN', // TENANT ADMIN 6 | DESIGNER = 'DESIGNER' 7 | } 8 | 9 | var byPass = false; 10 | class SecureUser implements ISecureUser { 11 | userName; 12 | userGroups; 13 | tenantId?; 14 | modelsOwner?; 15 | constructor(params: IUserInfo) { 16 | Object.assign(this, params); 17 | 18 | if (typeof process !=='undefined' && (process.env.REQUIRE_AUTHENTICATION === 'false' || process.env.ENFORCE_SECURITY === 'false')) { 19 | console.log('****Security is disabled as requested in .env****'); 20 | byPass = true; 21 | } 22 | else 23 | console.log('Security is enabled as requested in .env'); 24 | 25 | } 26 | static SystemUser() { 27 | return new SecureUser({ userName: 'system', userGroups: [USER_ROLE.SYSTEM], tenantId: null, modelsOwner: null }); 28 | } 29 | isAdmin(): boolean { 30 | if (byPass) 31 | return true; 32 | return (this.userGroups.includes(USER_ROLE.ADMIN) || this.userGroups.includes(USER_ROLE.SYSTEM)); 33 | } 34 | isSystem(): boolean { 35 | if (byPass) 36 | return true; 37 | return (this.userGroups.includes(USER_ROLE.SYSTEM)); 38 | } 39 | inGroup(userGroup) :boolean { 40 | if (byPass) 41 | return true; 42 | return (this.userGroups.includes(userGroup)) 43 | } 44 | /** 45 | * alters the query adding conditions based on security rules 46 | * @param query 47 | * @returns query 48 | */ 49 | qualifyInstances(query) { 50 | if (byPass) 51 | return query; 52 | if (this.tenantId) 53 | query['tenantId'] = this.tenantId; 54 | if (!this.isAdmin()) { 55 | 56 | const grpQuery = []; 57 | 58 | grpQuery.push({ 'items.assignee': null, 'items.candidateUsers': null, 'items.candidateGroups': null }); 59 | 60 | grpQuery.push({ 'items.assignee': this.userName }); 61 | grpQuery.push({ 'items.candidateUsers': this.userName }); 62 | 63 | this.userGroups.forEach(grp => { 64 | grpQuery.push({ 'items.candidateGroups': grp }); 65 | }); 66 | query['$or'] =grpQuery; 67 | } 68 | 69 | return query; 70 | } 71 | /** 72 | * alters the query adding conditions based on security rules 73 | * @param query 74 | * @returns query 75 | */ 76 | qualifyItems(query) { 77 | if (byPass) 78 | return query; 79 | return this.qualifyInstances(query); 80 | 81 | } 82 | /** 83 | * alters the query adding conditions based on security rules 84 | * @param query 85 | * @returns query 86 | */ 87 | 88 | qualifyStartEvents(query) { 89 | if (!this.isAdmin()) { 90 | const grpQuery = []; 91 | grpQuery.push({ "events.candidateUsers": null, "events.candidateGroups": null ,"events.lane": null }); 92 | 93 | grpQuery.push({ 'events.candidateUsers': this.userName }); 94 | 95 | this.userGroups.forEach(grp => { 96 | grpQuery.push({ 'events.candidateGroups': grp }); 97 | grpQuery.push({ 'events.lane': grp }); 98 | }); 99 | query['$or'] = grpQuery; 100 | } 101 | 102 | return query; 103 | 104 | } 105 | /** 106 | * alters the query adding conditions based on security rules 107 | * @param query 108 | * @returns query 109 | */ 110 | qualifyDeleteInstances(query) { 111 | if (this.isAdmin()) 112 | return this.qualifyInstances(query); 113 | else 114 | return false; 115 | } 116 | /** 117 | * alters the query adding conditions based on security rules 118 | * @param query 119 | * @returns query 120 | */ 121 | qualifyModels(query) { 122 | if (byPass) 123 | return true; 124 | if (this.modelsOwner) 125 | query['owner'] = this.modelsOwner; 126 | 127 | return query; 128 | } 129 | /** 130 | */ 131 | canModifyModel(name) { 132 | if (this.isAdmin()) 133 | return true; 134 | else 135 | return false; 136 | } 137 | /** 138 | */ 139 | canDeleteModel(name) { 140 | if (this.isAdmin()) 141 | return true; 142 | else 143 | return false; 144 | } 145 | /** 146 | * alters the query adding conditions based on security rules 147 | * @param query 148 | * @returns query 149 | */ 150 | async qualifyViewItems(query) { } 151 | async canInvoke(item) { } 152 | async canAssign(item) { } 153 | async canStart(name, startNodeId, user) { } 154 | } 155 | 156 | export {SecureUser,USER_ROLE , IUserInfo } -------------------------------------------------------------------------------- /src/API/SecureUser.ts: -------------------------------------------------------------------------------- 1 | import { QueryTranslator } from "../datastore/QueryTranslator"; 2 | import { ISecureUser, IUserInfo } from "../interfaces"; 3 | 4 | enum USER_ROLE { 5 | SYSTEM = 'SYSTEM', // SYSTEM ADMIN 6 | ADMIN = 'ADMIN', // TENANT ADMIN 7 | DESIGNER = 'DESIGNER' 8 | } 9 | /** 10 | * 11 | * Security Rules: 12 | * Security Rules are by-passed if 13 | * a. User is and Admin or System 14 | * b. .env REQUIRE_AUTHENTICATION === 'false' || process.env.ENFORCE_SECURITY 15 | * 16 | * User can View/Assign Item if: 17 | * 1. Item has no restrictions if (Assignee==null and CandidateUsers == null and CandidateGroups==null) 18 | * 2. If user is has a group indicated in item.CandidateGroups 19 | * 20 | * User can Execute Item -- If user is the Assignee 21 | * 22 | * Scenarios: 23 | * 1. User is Assignee 24 | * Can Invoke/Complete Task 25 | * Can Assign to another User 26 | * 2. User is not Assignee but Can View (see rules above) 27 | * Can Assign Task to either another user or self (Take Task) 28 | * Then User can Invoke Task 29 | * We may need to utilize Optimistic Lock pattern here, keep track of version # of the Instance and on update 30 | * Warn the User that instance information has changed since last seen 31 | * 32 | * */ 33 | var byPass = false; 34 | 35 | class SecureUser implements ISecureUser { 36 | userName; 37 | userGroups; 38 | tenantId?; 39 | modelsOwner?; 40 | constructor(params: IUserInfo) { 41 | Object.assign(this, params); 42 | 43 | if (typeof process !=='undefined' && (process.env.REQUIRE_AUTHENTICATION === 'false' || process.env.ENFORCE_SECURITY === 'false')) { 44 | console.log('****Security is disabled as requested in .env****'); 45 | byPass = true; 46 | } 47 | // else 48 | // console.log('Security is enabled as requested in .env'); 49 | 50 | // console.log('new SecureUser', this); 51 | 52 | } 53 | static SystemUser() { 54 | return new SecureUser({ userName: 'system', userGroups: [USER_ROLE.SYSTEM], tenantId: null, modelsOwner: null }); 55 | } 56 | isAdmin(): boolean { 57 | if (byPass) 58 | return true; 59 | return (this.userGroups.includes(USER_ROLE.ADMIN) || this.userGroups.includes(USER_ROLE.SYSTEM)); 60 | } 61 | isSystem(): boolean { 62 | if (byPass) 63 | return true; 64 | return (this.userGroups.includes(USER_ROLE.SYSTEM)); 65 | } 66 | inGroup(userGroup) :boolean { 67 | if (byPass) 68 | return true; 69 | return (this.userGroups.includes(userGroup)) 70 | } 71 | /** 72 | * alters the query adding conditions based on security rules 73 | * @param query 74 | * @returns query 75 | */ 76 | qualifyInstances(query) { 77 | if (byPass) 78 | return query; 79 | if (this.tenantId) 80 | query['tenantId'] = this.tenantId; 81 | if (!this.isAdmin()) { 82 | 83 | const grpQuery = []; 84 | /* old 85 | grpQuery.push({ 'items.assignee': null, 'items.candidateUsers': null, 'items.candidateGroups': null }); 86 | 87 | grpQuery.push({ 'items.assignee': this.userName }); 88 | grpQuery.push({ 'items.candidateUsers': this.userName, 'items.assignee': null }); 89 | 90 | this.userGroups.forEach(grp => { 91 | grpQuery.push({ 'items.candidateGroups': grp, 'items.assignee': null }); 92 | }); 93 | 94 | query['$or'] = grpQuery; 95 | */ 96 | grpQuery.push({ 'items.assignee': null, 'items.candidateUsers': null, 'items.candidateGroups': null }); 97 | 98 | grpQuery.push({ 'items.assignee': this.userName }); 99 | grpQuery.push({ 'items.candidateUsers': this.userName}); 100 | 101 | this.userGroups.forEach(grp => { 102 | grpQuery.push({ 'items.candidateGroups': grp}); 103 | }); 104 | 105 | query['$or'] = grpQuery; 106 | } 107 | 108 | return query; 109 | } 110 | /** 111 | * alters the query adding conditions based on security rules 112 | * @param query 113 | * @returns query 114 | */ 115 | qualifyItems(query) { 116 | if (byPass) 117 | return query; 118 | return this.qualifyInstances(query); 119 | 120 | } 121 | /** 122 | * alters the query adding conditions based on security rules 123 | * @param query 124 | * @returns query 125 | */ 126 | 127 | qualifyStartEvents(query) { 128 | if (!this.isAdmin()) { 129 | const grpQuery = []; 130 | grpQuery.push({ "events.candidateUsers": null, "events.candidateGroups": null ,"events.lane": null }); 131 | 132 | grpQuery.push({ 'events.candidateUsers': this.userName }); 133 | 134 | this.userGroups.forEach(grp => { 135 | grpQuery.push({ 'events.candidateGroups': grp }); 136 | grpQuery.push({ 'events.lane': grp }); 137 | }); 138 | query['$or'] = grpQuery; 139 | } 140 | 141 | return query; 142 | 143 | } 144 | /** 145 | * alters the query adding conditions based on security rules 146 | * @param query 147 | * @returns query 148 | */ 149 | qualifyDeleteInstances(query) { 150 | if (this.isAdmin()) 151 | return this.qualifyInstances(query); 152 | else 153 | return false; 154 | } 155 | /** 156 | * alters the query adding conditions based on security rules 157 | * @param query 158 | * @returns query 159 | */ 160 | qualifyModels(query) { 161 | if (byPass) 162 | return true; 163 | if (this.modelsOwner) 164 | query['owner'] = this.modelsOwner; 165 | 166 | return query; 167 | } 168 | /** 169 | */ 170 | canModifyModel(name) { 171 | if (this.isAdmin()) 172 | return true; 173 | else 174 | return false; 175 | } 176 | /** 177 | */ 178 | canDeleteModel(name) { 179 | if (this.isAdmin()) 180 | return true; 181 | else 182 | return false; 183 | } 184 | /** 185 | * alters the query adding conditions based on security rules 186 | * @param query 187 | * @returns query 188 | */ 189 | async qualifyViewItems(query) { 190 | 191 | } 192 | canInvoke(item) : boolean { 193 | 194 | if (this.isAdmin()) 195 | return true; 196 | else if (item.assignee == this.userName) 197 | return true; 198 | else 199 | return false; 200 | } 201 | canAssign(item) : boolean { 202 | 203 | if (this.isAdmin()) 204 | return true; 205 | else if (item.assignee == this.userName) 206 | return true; 207 | else 208 | { 209 | let query={}; 210 | const grpQuery = []; 211 | 212 | grpQuery.push({ 'items.assignee': null, 'items.candidateUsers': null, 'items.candidateGroups': null }); 213 | 214 | grpQuery.push({ 'items.candidateUsers': this.userName}); 215 | 216 | this.userGroups.forEach(grp => { 217 | grpQuery.push({ 'items.candidateGroups': grp}); 218 | }); 219 | 220 | query['$or'] = grpQuery; 221 | 222 | 223 | let trans=new QueryTranslator('items'); 224 | let dbQry=trans.translateCriteria(query); 225 | return trans.filterItem(item,dbQry); 226 | } 227 | } 228 | async canStart(name, startNodeId, user) { } 229 | } 230 | const SystemUser = new SecureUser({ userName: 'system', userGroups: [USER_ROLE.SYSTEM], tenantId: null, modelsOwner: null }); 231 | 232 | export {SecureUser,USER_ROLE , IUserInfo, SystemUser } -------------------------------------------------------------------------------- /src/API/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './API'; 3 | export * from './SecureUser'; 4 | 5 | -------------------------------------------------------------------------------- /src/common/DefaultConfiguration.ts: -------------------------------------------------------------------------------- 1 | 2 | import { ModelsDatastore } from '../datastore/ModelsDatastore'; 3 | import { DefaultAppDelegate } from '../engine/DefaultAppDelegate'; 4 | 5 | import { IConfiguration, DataStore, NoCacheManager,ILogger, IModelsDatastore, 6 | IAppDelegate, IDataStore, 7 | ScriptHandler} from '../'; 8 | import { Logger } from './' 9 | import { Script } from 'vm'; 10 | 11 | export class Configuration implements IConfiguration { 12 | definitionsPath?: string; 13 | templatesPath?: string; 14 | timers: { forceTimersDelay: number; precision: number; }; 15 | database: { 16 | MongoDB: { 17 | db_url: string; 18 | db: string; 19 | Locks_collection:'wf_locks'; 20 | Instance_collection:'wf_instances'; 21 | Archive_collection:'wf_archives'; 22 | }; 23 | loopbackRepositories?:any; 24 | }; 25 | logger: ILogger; 26 | apiKey: string; 27 | sendGridAPIKey: string; 28 | definitions(server) { 29 | return new ModelsDatastore(server); 30 | } 31 | appDelegate(server) :IAppDelegate { 32 | return new DefaultAppDelegate(server); 33 | } 34 | dataStore(server) { 35 | return new DataStore(server); 36 | } 37 | cacheManager(server) { 38 | return new NoCacheManager(server); 39 | } 40 | scriptHandler(server) { 41 | return new ScriptHandler(); 42 | } 43 | 44 | 45 | constructor({ 46 | definitionsPath, templatesPath, timers, database, apiKey, 47 | logger, 48 | definitions, 49 | appDelegate, 50 | dataStore,cacheManager,scriptHandler}) { 51 | this.definitionsPath = definitionsPath; 52 | this.templatesPath = templatesPath; 53 | this.timers = timers; 54 | this.database = database; 55 | this.apiKey = apiKey; 56 | this.logger = logger; 57 | this.definitions = definitions; 58 | this.appDelegate = appDelegate; 59 | this.dataStore = dataStore; 60 | this.cacheManager = cacheManager; 61 | this.scriptHandler=scriptHandler; 62 | 63 | } 64 | 65 | } 66 | export const defaultConfiguration = new Configuration( 67 | { 68 | definitionsPath: typeof __dirname !== 'undefined' ? __dirname + '/processes/' : undefined, 69 | templatesPath: typeof __dirname !== 'undefined' ? __dirname +'/emailTemplates' : undefined, 70 | timers: { 71 | forceTimersDelay: 1000, 72 | precision: 3000, 73 | }, 74 | database: { 75 | MongoDB: 76 | { 77 | db_url: "mongodb://localhost:27017?retryWrites=true&w=majority", 78 | db: 'bpmn' 79 | } 80 | }, 81 | apiKey: '1234', 82 | logger: function (server) { 83 | new Logger(server); 84 | }, 85 | definitions: function (server) { 86 | return new ModelsDatastore(server); 87 | }, 88 | appDelegate: function (server) { 89 | return new DefaultAppDelegate(server); 90 | }, 91 | dataStore: function (server) { 92 | return new DataStore(server); 93 | }, 94 | cacheManager: function (server) { 95 | return new NoCacheManager(server); 96 | }, 97 | scriptHandler: function (server) { 98 | return new ScriptHandler(); 99 | } 100 | 101 | }); 102 | -------------------------------------------------------------------------------- /src/common/Logger.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { ILogger } from "../"; 4 | 5 | class Logger implements ILogger { 6 | 7 | debugMsgs = []; 8 | toConsole = true; 9 | toFile = null; 10 | callback = null; 11 | level=0; 12 | 13 | constructor({ toConsole=true, toFile='', callback=null }) { 14 | this.setOptions({ toConsole, toFile, callback }); 15 | } 16 | setOptions({ toConsole, toFile, callback }) { 17 | 18 | this.toConsole = toConsole; 19 | this.toFile = toFile; 20 | this.callback = callback; 21 | } 22 | msg(message , type='log') { 23 | let level=this.level>-1 ? ' '.repeat(this.level):''; 24 | if (this.toConsole) 25 | console.log(level+message); 26 | if (this.callback) { 27 | this.callback(message, type); 28 | } 29 | 30 | if (this.toFile !== '') { 31 | const fs = require('fs'); 32 | fs.appendFileSync(this.toFile, message); 33 | } 34 | this.debugMsgs.push({date:new Date(),message, type,level:this.level}); 35 | return ({date:new Date(),message,type,level:this.level}); 36 | } 37 | clear() { 38 | 39 | this.debugMsgs = []; 40 | } 41 | get() { 42 | 43 | return this.debugMsgs; 44 | } 45 | info(...message) { 46 | return this.msg(this.toString(...message), 'info'); 47 | } 48 | debug(...message) 49 | { 50 | return this.msg(this.toString(...message),'debug'); 51 | } 52 | warn(...message) { 53 | return this.msg(this.toString(...message),'warn'); 54 | } 55 | log(...message) { 56 | return this.msg(this.toString(...message),'log'); 57 | } 58 | logS(...message) { 59 | 60 | let msg=this.msg(this.toString(...message),'log'); 61 | this.level++; 62 | return msg; 63 | } 64 | logE(...message) { 65 | let msg=this.msg(this.toString(...message),'log'); 66 | this.level--; 67 | return msg; 68 | } 69 | toString(...args) { 70 | var out = ''; 71 | for (var i = 0; i < args.length; i++) { 72 | var val = args[i]; 73 | if (i > 0) 74 | out += ' ,'; 75 | 76 | if (typeof val === 'undefined') { 77 | out += val; 78 | } 79 | else { 80 | var cls = val.constructor.name; 81 | if (cls === 'String') 82 | out += val; 83 | else if (cls === 'Array' && val.length==1) 84 | { 85 | if (val[0].constructor.name==='Array') 86 | out += JSON.stringify(val[0]); 87 | else 88 | out +=val[0]; 89 | 90 | } 91 | else 92 | out += cls + " " + JSON.stringify(val, null, 2); 93 | } 94 | } 95 | if (out.substring(0,1)=='^') { 96 | out=out.substring(1); 97 | this.level=0; 98 | } 99 | return out; 100 | } 101 | reportError(err) { 102 | if (typeof err === 'object') { 103 | if (err.message) { 104 | this.msg(err.message, 'error'); 105 | console.log('\nError Message: ' + err.message) 106 | } 107 | if (err.stack) { 108 | console.log('\nStacktrace:') 109 | console.log('====================') 110 | console.log(err.stack); 111 | this.log(err.stack); 112 | } 113 | } else { 114 | this.msg(err, 'error'); 115 | } 116 | 117 | } 118 | error(err) { 119 | throw new Error(err); 120 | } 121 | async save(filename,flag='w') { 122 | const fs = require('fs'); 123 | console.log("writing to:" + filename + " " + this.debugMsgs.length); 124 | let id = fs.openSync(filename, flag, 666); 125 | { 126 | fs.writeSync(id,'Started at: '+new Date().toISOString() + "\n", null, 'utf8'); 127 | 128 | let l = 0; 129 | for (l = 0; l < this.debugMsgs.length; l++) { 130 | let msg = this.debugMsgs[l]; 131 | if (msg.type == 'error') { 132 | let line = msg.type + ": at line " + (l+1) + " " + msg.message; 133 | fs.writeSync(id, line + "\n", null, 'utf8'); 134 | } 135 | } 136 | for (l = 0; l < this.debugMsgs.length; l++) { 137 | let msg = this.debugMsgs[l]; 138 | let line; 139 | if (msg.type == 'eror') 140 | line = msg.type + ":" + msg.message; 141 | else 142 | line = msg.message; 143 | 144 | let level=this.level>-1 ? ' '.repeat(this.level):''; 145 | fs.writeSync(id,level+line + "\n", null, 'utf8'); 146 | } 147 | 148 | fs.closeSync(id ); 149 | this.clear(); 150 | 151 | } 152 | } 153 | async saveForInstance(instanceId) { 154 | if (process.env['logFolder']) 155 | await this.save(process.env['logFolder']+'/'+instanceId+'.log', 'a'); 156 | 157 | } 158 | } 159 | 160 | export { Logger }; 161 | 162 | -------------------------------------------------------------------------------- /src/common/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './Logger'; 3 | export * from './DefaultConfiguration'; 4 | export * from './timer'; 5 | 6 | -------------------------------------------------------------------------------- /src/common/timer.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | function dateDiff(dateStr) { 5 | 6 | if (dateStr == null) 7 | return ''; 8 | var endDate = new Date(); 9 | var startTime = new Date(dateStr); 10 | var time1 = endDate.getTime(); 11 | var time2 = startTime as unknown as number; 12 | 13 | var seconds = Math.abs(time1- time2) / 1000; 14 | 15 | 16 | // get total seconds between the times 17 | var delta = seconds; //Math.abs(date_future - date_now) / 1000; 18 | 19 | // calculate (and subtract) whole days 20 | var days = Math.floor(delta / 86400); 21 | delta -= days * 86400; 22 | 23 | // calculate (and subtract) whole hours 24 | var hours = Math.floor(delta / 3600) % 24; 25 | delta -= hours * 3600; 26 | 27 | // calculate (and subtract) whole minutes 28 | var minutes = Math.floor(delta / 60) % 60; 29 | delta -= minutes * 60; 30 | 31 | // what's left is seconds 32 | var seconds = Math.floor(delta % 60); // in theory the modulus is not required 33 | if (days > 0) 34 | return (days + " days"); 35 | if (hours > 0) 36 | return (hours + " hours"); 37 | if (minutes > 0) 38 | return (minutes + " minutes"); 39 | return (seconds + " seconds"); 40 | } 41 | 42 | export { dateDiff } 43 | -------------------------------------------------------------------------------- /src/datastore/InstanceLocker.ts: -------------------------------------------------------------------------------- 1 | import { DataStore } from './';; 2 | 3 | 4 | const COLLECTION='wf_locks'; 5 | const WAIT=1500; 6 | const MAX_TRIES=20; 7 | 8 | class InstanceLocker { 9 | dataStore; 10 | 11 | constructor(dataStore) { 12 | this.dataStore=dataStore; 13 | } 14 | async lock(id) { 15 | 16 | var counter=0; 17 | var failed=true; 18 | while(counter++ { 27 | 28 | if (n.type == 'bpmn:StartEvent') { 29 | const event = new EventData(); 30 | event.elementId = n.id; 31 | event.name = n.name; 32 | event.type = n.type; 33 | event.lane = n.lane; 34 | event.subType = n.subType; 35 | event.candidateGroups=n.candidateGroups; 36 | event.candidateUsers=n.candidateUsers; 37 | event.processId = n.processId; 38 | let doc; 39 | if (n.def.documentation) 40 | { 41 | n.def.documentation.forEach(d=>{ doc=d.text;}) 42 | event.documentation = doc; 43 | 44 | } 45 | 46 | let timer = n.hasTimer(); 47 | if (timer) { 48 | event.timeDue = timer.timeDue(); 49 | event.subType = 'Timer'; 50 | event.expression = timer.timeCycle; 51 | if (!event.expression) 52 | event.expression = timer.duration; 53 | event.referenceDateTime = new Date().getTime(); 54 | } 55 | let msg = n.hasMessage(); 56 | if (msg) { 57 | event.messageId = msg.messageId; 58 | event.subType = 'Message'; 59 | //console.log('timer:' + timer.timeDueInSeconds()); 60 | } 61 | let signal = n.hasSignal(); 62 | if (signal) { 63 | event.signalId = signal.signalId; 64 | event.subType = 'Signal'; 65 | } 66 | this.events.push(event); 67 | } 68 | 69 | }); 70 | 71 | definition.processes.forEach(p => { 72 | let proc = new ProcessData(); 73 | proc.id = p.def.id; 74 | proc.name = p.def.name; 75 | proc.isExecutable = p.def.isExecutable; 76 | proc.candidateStarterGroups=p.def.candidateStarterGroups; 77 | proc.candidateStarterUsers=p.def.candidateStarterUsers; 78 | proc.historyTimeToLive=p.def.historyTimeToLive; 79 | proc.isStartableInTasklist=p.def.isStartableInTasklist; 80 | let doc; 81 | if (p.def.documentation) 82 | { 83 | p.def.documentation.forEach(d=>{ doc=d.text;}) 84 | proc.documentation = doc; 85 | 86 | } 87 | this.processes.push(proc); 88 | }); 89 | } 90 | } 91 | class ProcessData implements IProcessData { 92 | id; 93 | name; 94 | documentation; 95 | isExecutable; 96 | candidateStarterGroups; 97 | candidateStarterUsers; 98 | historyTimeToLive; 99 | isStartableInTasklist; 100 | } 101 | class EventData implements IEventData { 102 | elementId; 103 | type; 104 | subType; 105 | name; 106 | processId; 107 | signalId; 108 | messageId; 109 | // timer info 110 | expression; 111 | expressionFormat; // cron/iso 112 | referenceDateTime; // start time of event or last time timer ran 113 | maxRepeat; 114 | repeatCount; 115 | timeDue; 116 | lane; 117 | candidateGroups; 118 | candidateUsers; 119 | documentation; 120 | } 121 | 122 | 123 | export {ProcessData, EventData , BpmnModelData } -------------------------------------------------------------------------------- /src/datastore/ModelsDatastore.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Definition } from "../elements"; 3 | import { BPMNServer } from "../server"; 4 | import {ModelsDatastoreDB} from "./ModelsDatastoreDB"; 5 | 6 | import { IModelsDatastore } from "../interfaces/"; 7 | 8 | 9 | class ModelsDatastore extends ModelsDatastoreDB implements IModelsDatastore { 10 | 11 | constructor(server: BPMNServer) { 12 | super(server); 13 | 14 | } 15 | get definitionsPath() { return this.server.configuration.definitionsPath; } 16 | 17 | 18 | async import(data,owner=null) { 19 | return await super.import(data); 20 | 21 | } 22 | 23 | async getList(query=null): Promise { 24 | 25 | let files = []; 26 | const fs = require('fs'); 27 | fs.readdirSync(this.definitionsPath).forEach(file => { 28 | const path = require('path') 29 | if (path.extname(file) == '.bpmn') { 30 | let name = path.basename(file); 31 | name = name.substring(0, name.length - 5);; 32 | files.push({name,saved:null}); 33 | } 34 | }); 35 | 36 | return files; 37 | } 38 | 39 | /* 40 | * loads a definition 41 | * 42 | */ 43 | async load(name,owner=null) : Promise { 44 | 45 | const source = await this.getSource(name); 46 | //const rules = this.getFile(name, 'rules'); 47 | 48 | const definition = new Definition(name, source, this.server); 49 | await definition.load(); 50 | return definition; 51 | } 52 | 53 | private getPath(name, type,owner=null) { 54 | 55 | return this.definitionsPath + name + '.' + type; 56 | } 57 | 58 | private getFile(name, type,owner=null) { 59 | const fs = require('fs'); 60 | let file = fs.readFileSync(this.getPath(name,type), 61 | { encoding: 'utf8', flag: 'r' }); 62 | return file; 63 | 64 | } 65 | private saveFile(name, type , data,owner=null) { 66 | let fullpath = this.getPath(name, type); 67 | const fs = require('fs'); 68 | fs.writeFile(fullpath, data, function (err) { 69 | if (err) throw err; 70 | }); 71 | 72 | } 73 | async getSource(name,owner=null): Promise { 74 | 75 | return this.getFile(name, 'bpmn'); 76 | 77 | } 78 | async getSVG(name,owner=null): Promise { 79 | return this.getFile(name, 'svg'); 80 | } 81 | 82 | async save(name, bpmn, svg?,owner=null): Promise { 83 | 84 | this.saveFile(name, 'bpmn', bpmn); 85 | if (svg) 86 | this.saveFile(name, 'svg', svg); 87 | 88 | await super.save(name, bpmn, svg); 89 | 90 | return true; 91 | 92 | } 93 | 94 | async deleteModel(name: any,owner=null): Promise { 95 | const fs = require('fs'); 96 | await super.deleteModel(name); 97 | await fs.unlink(this.definitionsPath + name + '.bpmn', function (err) { 98 | if (err) console.log('ERROR: ' + err); 99 | }); 100 | await fs.unlink(this.definitionsPath + name + '.svg',function (err) { 101 | if (err) console.log('ERROR: ' + err); 102 | }); 103 | } 104 | async renameModel(name: any, newName: any,owner=null): Promise { 105 | const fs = require('fs'); 106 | await super.renameModel(name, newName); 107 | await fs.rename(this.definitionsPath + name + '.bpmn', this.definitionsPath + newName + '.bpmn', function (err) { 108 | if (err) console.log('ERROR: ' + err); 109 | }); 110 | await fs.rename(this.definitionsPath + name + '.svg', this.definitionsPath + newName + '.svg', function (err) { 111 | if (err) console.log('ERROR: ' + err); 112 | }); 113 | return true; 114 | } 115 | /** 116 | * 117 | * reconstruct the models database from files 118 | * 119 | * use when modifying the files manually or importing new environment 120 | * 121 | * */ 122 | async rebuild(model=null) { 123 | 124 | try { 125 | if (model) 126 | return this.rebuildModel(model); 127 | let filesList = await this.getList(); 128 | const models = new Map(); 129 | 130 | filesList.forEach(f => { 131 | const path=this.definitionsPath + f['name'] + '.bpmn'; 132 | const fs = require('fs'); 133 | var stats = fs.statSync(path); 134 | var mtime = stats.mtime; 135 | models.set(f['name'], mtime); 136 | }); 137 | const dbList = await super.get(); 138 | dbList.forEach(model => { 139 | const name = model['name']; 140 | const saved = new Date(model['saved']); 141 | const entry = models.get(name); 142 | if (entry) { 143 | if (saved.getTime() > entry.getTime()) { 144 | models.delete(name); 145 | } 146 | } 147 | else { 148 | super.deleteModel(name); 149 | } 150 | }); 151 | let i; 152 | 153 | for (const entry of models.entries()) { 154 | const name = entry[0]; 155 | await this.rebuildModel(name); 156 | } 157 | } 158 | catch(exc) 159 | { 160 | console.log('rebuild error'); 161 | throw exc; 162 | } 163 | } 164 | private async rebuildModel(name) { 165 | console.log("rebuilding " + name); 166 | let source = await this.getSource(name); 167 | let svg; 168 | try { 169 | svg = await this.getSVG(name); 170 | } 171 | catch (exc) { 172 | //console.log(exc); 173 | } 174 | await super.save(name, source, svg); 175 | 176 | } 177 | 178 | } 179 | 180 | export {ModelsDatastore } -------------------------------------------------------------------------------- /src/datastore/ModelsDatastoreDB.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Definition } from "../elements"; 3 | import { BPMNServer } from "../server"; 4 | 5 | import { ServerComponent } from "../server/ServerComponent"; 6 | import { IBpmnModelData, IModelsDatastore, IEventData } from "../interfaces/"; 7 | import { BpmnModelData } from "./ModelsData"; 8 | import { QueryTranslator } from "./QueryTranslator"; 9 | 10 | const Definition_collection = 'wf_models'; 11 | const Events_collection = 'wf_events'; 12 | 13 | class ModelsDatastoreDB extends ServerComponent implements IModelsDatastore { 14 | dbConfiguration; 15 | db; 16 | 17 | constructor(server: BPMNServer) { 18 | super(server); 19 | 20 | this.dbConfiguration = this.configuration.database.MongoDB; 21 | const MongoDB = require('./MongoDB').MongoDB; 22 | this.db = new MongoDB(this.dbConfiguration, this.logger); 23 | 24 | } 25 | async get(query={}): Promise { 26 | 27 | const list=await this.db.find(this.dbConfiguration.db, Definition_collection,query, {}); 28 | return list; 29 | 30 | } 31 | async getList(query={}): Promise { 32 | 33 | var records = await this.db.find(this.dbConfiguration.db, Definition_collection,query, {}); 34 | 35 | // this.logger.log('find events for ' + " recs:" + records.length); 36 | const list = []; 37 | 38 | records.forEach(r => { list.push({ name: r.name }); }); 39 | return list; 40 | } 41 | /* 42 | * loads a definition 43 | * 44 | */ 45 | async load(name,owner=null): Promise { 46 | console.log('loading ', name, 'from db'); 47 | let data = await this.loadModel(name); 48 | const definition = new Definition(name, data.source, this.server); 49 | await definition.load(); 50 | return definition; 51 | } 52 | async getSource(name,owner=null) { 53 | let model = await this.loadModel(name); 54 | return model.source; 55 | 56 | } 57 | async getSVG(name,owner=null) { 58 | let model = await this.loadModel(name); 59 | return model.svg; 60 | 61 | } 62 | /* 63 | * loads a definition data record from DB 64 | * 65 | */ 66 | async loadModel(name,owner=null): Promise { 67 | 68 | var records = await this.db.find(this.dbConfiguration.db, Definition_collection, { name: name }, {}); 69 | 70 | this.logger.log('find model for ' + name + " recs:" + records.length); 71 | 72 | return records[0]; 73 | 74 | } 75 | 76 | async save(name, source, svg,owner=null): Promise { 77 | let bpmnModelData: BpmnModelData = new BpmnModelData(name, source, svg, null, null); 78 | let definition = new Definition(bpmnModelData.name, bpmnModelData.source, this.server); 79 | try { 80 | await definition.load(); 81 | 82 | bpmnModelData.parse(definition); 83 | await this.saveModel(bpmnModelData,owner); 84 | 85 | return bpmnModelData; 86 | } 87 | catch(exc) 88 | { 89 | console.log('error in saving',name,exc); 90 | throw exc; 91 | return null; 92 | } 93 | 94 | } 95 | async findEvents(query,owner=null): Promise { 96 | 97 | let projection = {}; // this.getProjection(query); 98 | 99 | const events = []; 100 | let trans; 101 | let newQuery=query; 102 | if (query) { 103 | trans = new QueryTranslator('events'); 104 | newQuery = trans.translateCriteria(query); 105 | } 106 | 107 | var records = await this.db.find(this.dbConfiguration.db, Definition_collection, newQuery, projection); 108 | 109 | // this.logger.log('...find events for ' + JSON.stringify(query) + "=>" + JSON.stringify(newQuery) + " recs:" + records.length); 110 | 111 | records.forEach(rec => { 112 | rec.events.forEach(ev => { 113 | let pass = true; 114 | if (query) { 115 | pass = trans.filterItem(ev, newQuery); 116 | 117 | } 118 | if (pass) { 119 | ev.modelName = rec.name; 120 | ev._id = rec._id; 121 | events.push(ev); 122 | } 123 | }); 124 | }); 125 | 126 | return events; 127 | 128 | } 129 | private getProjection(query) { 130 | 131 | let match = {}; 132 | let projection = {}; 133 | { 134 | Object.keys(query).forEach(key => { 135 | if (key.startsWith('events.')) { 136 | let val = query[key]; 137 | key = key.replace('events.', ''); 138 | match[key] = val; 139 | } 140 | }); 141 | if (Object.keys(match).length == 0) 142 | projection = { id: 1, name: 1 }; 143 | else 144 | projection = { id: 1, name: 1, "events": { $elemMatch: match } }; 145 | } 146 | return projection; 147 | } 148 | // db.collection.createIndex({ "a.loc": 1, "a.qty": 1 }, { unique: true }) 149 | /** 150 | * first time installation of DB 151 | * 152 | * creates a new collection and add an index 153 | * 154 | * */ 155 | async install() { 156 | return await this.db.createIndex(this.dbConfiguration.db, Definition_collection, { name: 1 , owner: 1 }, { unique: true }); 157 | } 158 | async import(data,owner=null) { 159 | return await this.db.insert(this.dbConfiguration.db, Definition_collection, data); 160 | 161 | } 162 | async updateTimer(name,owner=null): Promise { 163 | 164 | const source = await this.getSource(name,owner); 165 | let model: BpmnModelData = new BpmnModelData(name, source, null, null, null); 166 | let definition = new Definition(model.name, model.source, this.server); 167 | await definition.load(); 168 | 169 | model.parse(definition); 170 | 171 | await this.db.update(this.dbConfiguration.db, Definition_collection, 172 | { name: model.name }, 173 | { 174 | $set: 175 | { 176 | events: model.events 177 | } 178 | }, { upsert: false }); 179 | 180 | 181 | this.logger.log("updating model"); 182 | 183 | this.logger.log('DataStore:saving Complete'); 184 | 185 | return true; 186 | 187 | } 188 | async saveModel(model: IBpmnModelData,owner=null): Promise { 189 | 190 | this.logger.log("Saving Model " + model.name); 191 | 192 | var recs; 193 | model.saved = new Date(); 194 | 195 | await this.db.update(this.dbConfiguration.db, Definition_collection, 196 | { name: model.name , owner:owner}, 197 | { 198 | $set: 199 | { 200 | name: model.name, owner:owner, saved: model.saved, source: model.source, svg: model.svg, processes: model.processes, events: model.events 201 | } 202 | }, { upsert: true }); 203 | 204 | 205 | return true; 206 | 207 | } 208 | async deleteModel(name,owner=null) { 209 | 210 | await this.db.remove(this.dbConfiguration.db, Definition_collection, { name: name }); 211 | 212 | } 213 | async renameModel(name, newName,owner=null) { 214 | 215 | await this.db.update(this.dbConfiguration.db, Definition_collection, 216 | { name: name }, 217 | { 218 | $set: 219 | { 220 | name: newName 221 | } 222 | }, { upsert: false }); 223 | 224 | 225 | this.logger.log("updating model"); 226 | 227 | this.logger.log('DataStore:saving Complete'); 228 | 229 | return true; 230 | } 231 | async export(name, folderPath,owner=null) { 232 | const fs = require('fs'); 233 | 234 | let model = await this.loadModel(name,owner); 235 | 236 | let fullpath = folderPath + "/" + name + ".bpmn"; 237 | 238 | fs.writeFile(fullpath, model.source, function (err) { 239 | if (err) throw err; 240 | console.log(`Saved bpmn to ${fullpath}`); 241 | }); 242 | 243 | fullpath = folderPath + "/" + name + ".svg"; 244 | 245 | fs.writeFile(fullpath, model.svg, function (err) { 246 | if (err) throw err; 247 | console.log(`Saved svg to ${fullpath}`); 248 | }); 249 | } 250 | async rebuild(model=null) { 251 | } 252 | } 253 | 254 | export { ModelsDatastoreDB } -------------------------------------------------------------------------------- /src/datastore/MongoDB.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Replace update() with updateOne(), updateMany(), or replaceOne() 3 | Replace remove() with deleteOne() or deleteMany(). 4 | Replace count() with countDocuments(), unless you want to count how many documents are in the whole collection(no filter).In the latter case, use estimatedDocumentCount(). 5 | */ 6 | class MongoDB { 7 | client; 8 | dbConfig; 9 | logger; 10 | operation; 11 | constructor(dbConfig,logger) { 12 | this.dbConfig = dbConfig; 13 | this.logger = logger; 14 | 15 | } 16 | profilerStart(operation) { 17 | if (process.env.ENABLE_PROFILER === 'true') 18 | console.time(operation); 19 | this.operation=operation; 20 | } 21 | profilerEnd() { 22 | if (process.env.ENABLE_PROFILER === 'true') 23 | console.timeEnd(this.operation); 24 | 25 | } 26 | async getClient() { 27 | 28 | if (this.client == null) { 29 | this.client = await this.connect(); 30 | } 31 | return this.client; 32 | } 33 | async find( dbName, collName, qry, projection=null,sort=null) { 34 | 35 | var client = await this.getClient(); 36 | 37 | const db = client.db(dbName); 38 | const collection = db.collection(collName); 39 | const self=this; 40 | 41 | return new Promise(function (resolve, reject) { 42 | 43 | // Use connect method to connect to the Server 44 | 45 | let cursor; 46 | self.profilerStart('>mongo.find:'+collName); 47 | if (projection) 48 | cursor = collection.find(qry).project(projection); 49 | else if (sort) 50 | cursor = collection.find(qry).sort(sort); 51 | else 52 | cursor = collection.find(qry); 53 | 54 | self.profilerEnd(); 55 | cursor.toArray(function (err, docs) { 56 | // Do async job 57 | if (err) { 58 | reject(err); 59 | } else { 60 | resolve(docs); 61 | } 62 | }) 63 | }) 64 | } 65 | 66 | // db.collection.createIndex( { "a.loc": 1, "a.qty": 1 }, { unique: true } ) 67 | 68 | async createIndex(dbName, collName, index, unique = {} ) { 69 | var client = await this.getClient(); 70 | 71 | const db = client.db(dbName); 72 | const collection = db.collection(collName); 73 | 74 | return new Promise(function (resolve, reject) { 75 | 76 | collection.createIndex(index, unique , function (err, result) { 77 | if (err) { 78 | if (err.code==85) 79 | console.log('index for '+JSON.stringify(index)+' already exists for collection "'+collName+'"' ); 80 | else 81 | console.log('error',err); 82 | resolve(null); 83 | } else 84 | { 85 | // self.logger.log(" inserted " + result.result); 86 | console.log('index named "'+result+'" was created for collection "'+collName+'"'); 87 | resolve(result); 88 | } 89 | }); 90 | }); 91 | 92 | } 93 | async insert(dbName, collName, docs) { 94 | 95 | var client = await this.getClient(); 96 | 97 | // Get the documents collection 98 | const db = client.db(dbName); 99 | const collection = db.collection(collName); 100 | // Insert some documents 101 | 102 | let self = this; 103 | return new Promise(function (resolve, reject) { 104 | 105 | self.profilerStart('>mongo.insert:'+collName); 106 | collection.insertMany(docs, function (err, result) { 107 | self.profilerEnd(); 108 | if (err) { 109 | reject(err); 110 | } else { 111 | // self.logger.log(" inserted " + result.result.n); 112 | // console.log(result); 113 | resolve(result.result.n); 114 | } 115 | }); 116 | }); 117 | 118 | } 119 | 120 | async update(dbName, collName, query, updateObject, options = {}) { 121 | 122 | var client = await this.getClient(); 123 | 124 | // Get the documents collection 125 | const db = client.db(dbName); 126 | const collection = db.collection(collName); 127 | // Insert some documents 128 | let self = this; 129 | return new Promise(function (resolve, reject) { 130 | 131 | self.profilerStart('>mongo.update:'+collName); 132 | collection.updateOne(query, updateObject, options, 133 | function (err, result) { 134 | self.profilerEnd(); 135 | 136 | if (err) { 137 | reject(err); 138 | } else { 139 | self.logger.log(" updated " + JSON.parse(result).n ); 140 | resolve(JSON.parse(result).n ); 141 | } 142 | }); 143 | }); 144 | } 145 | async update2(dbName, collName, query, updateObject, options = {}) { 146 | 147 | var client = await this.getClient(); 148 | 149 | // Get the documents collection 150 | const db = client.db(dbName); 151 | const collection = db.collection(collName); 152 | // Insert some documents 153 | let self = this; 154 | return new Promise(function (resolve, reject) { 155 | 156 | self.profilerStart('>mongo.update:'+collName); 157 | collection.update(query, updateObject, options, 158 | function (err, result) { 159 | self.profilerEnd(); 160 | if (err) { 161 | reject(err); 162 | } else { 163 | self.logger.log(" updated " + JSON.parse(result).n); 164 | resolve(JSON.parse(result).n); 165 | } 166 | }); 167 | }); 168 | } 169 | 170 | async remove(dbName, collName, query) { 171 | 172 | var client = await this.getClient(); 173 | 174 | // Get the documents collection 175 | const db = client.db(dbName); 176 | const collection = db.collection(collName); 177 | // Insert some documents 178 | 179 | let self = this; 180 | return new Promise(function (resolve, reject) { 181 | 182 | self.profilerStart('>mongo.remove:'+collName); 183 | collection.deleteMany(query, 184 | function (err, result) { 185 | self.profilerEnd(); 186 | 187 | if (err) { 188 | self.logger.log("error " + err); 189 | reject(err); 190 | } else { 191 | 192 | self.logger.log("remove done for " + JSON.parse(result).n + " docs in " + collName); 193 | 194 | resolve(result); 195 | } 196 | }); 197 | }); 198 | } 199 | 200 | async removeById(dbName,collName,id) { 201 | 202 | var client = await this.getClient(); 203 | 204 | // Get the documents collection 205 | const db = client.db(dbName); 206 | const collection = db.collection(collName); 207 | // Insert some documents 208 | 209 | let self = this; 210 | return new Promise(function (resolve, reject) { 211 | 212 | const MongoDb = require('mongodb'); 213 | collection.deleteOne({ _id: new MongoDb.ObjectID(id) }, 214 | function (err, result) { 215 | if (err) { 216 | self.logger.log("error " + err); 217 | reject(err); 218 | } else { 219 | 220 | self.logger.log("remove done for " + id + " >" + JSON.parse(result).n ); 221 | 222 | resolve(result); 223 | } 224 | }); 225 | }); 226 | } 227 | 228 | async connect() { 229 | // Return new promise 230 | const MongoClient = require('mongodb').MongoClient; 231 | 232 | const client = new MongoClient(this.dbConfig.db_url , { useUnifiedTopology: true }); 233 | 234 | return new Promise(function (resolve, reject) { 235 | 236 | // Use connect method to connect to the Server 237 | client.connect(function (err) { 238 | // Do async job 239 | if (err) { 240 | reject(err); 241 | client.close(); 242 | } else { 243 | resolve(client); 244 | } 245 | }) 246 | }) 247 | } 248 | } 249 | 250 | export { MongoDB }; -------------------------------------------------------------------------------- /src/datastore/QueryTranslator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * 4 | input Query: 5 | 6 | ```json 7 | { "items.status":"wait", 8 | "name": "Buy Used Car with Lanes", 9 | "$or":[ 10 | {"items.candidateGroups":"Owner"}, 11 | {"items.candidateUsers":"User1"} 12 | ] 13 | } 14 | ``` 15 | /* 16 | 17 | MongoQuery: 18 | 19 | ```json 20 | { "name":"Buy Used Car with Lanes", 21 | "$or":[ 22 | {"items":{"$elemMatch":{"candidateGroups":"Owner"}}}, 23 | {"items":{"$elemMatch":{"candidateUsers":"User1"}}} 24 | ], 25 | "items":{"$elemMatch":{"status":"wait"} 26 | } 27 | ``` 28 | 29 | And filter items by performing the Query on each Instance Item 30 | 31 | * 32 | * Supported Operators: 33 | * - $or 34 | * - $lte 35 | * - $lt 36 | * - $gte 37 | * - $gt 38 | * - $eq 39 | * Missing the following: 40 | * - $ne 41 | * - $regex 42 | * - $in 43 | * - $and 44 | * 45 | * https://www.mongodb.com/docs/manual/reference/operator/query/ 46 | * 47 | * */ 48 | class QueryTranslator { 49 | 50 | childName; 51 | constructor(childName) { 52 | this.childName = childName; 53 | } 54 | translateCriteria(query) { 55 | 56 | let match = {}; 57 | let hasMatch = false; 58 | let newQuery = {}; 59 | Object.keys(query).forEach(key => { 60 | let val = query[key]; 61 | if (key=='$or') 62 | { 63 | let predicates=[]; 64 | let res; 65 | val.forEach(predicate => { 66 | res = this.translateCriteria(predicate); 67 | predicates.push(res); 68 | }); 69 | newQuery['$or']= predicates; 70 | } 71 | else if (key.startsWith(this.childName+'.')) { 72 | key = key.replace(this.childName + '.', ''); 73 | match[key] = val; 74 | hasMatch = true; 75 | } 76 | else 77 | newQuery[key] = val; 78 | }); 79 | 80 | if (hasMatch) { 81 | newQuery[this.childName] = { $elemMatch: match }; 82 | } 83 | return newQuery; 84 | } 85 | 86 | private filterOr(item, condition) { 87 | let pass = false; 88 | for (let c = 0; c < condition.length; c++) { 89 | pass = this.filterItem(item, condition[c]); 90 | if (pass == true) // or 91 | break; 92 | } 93 | return pass; 94 | } 95 | public filterItem(item,query) 96 | { 97 | // console.log('--filterItem--', item.seq, JSON.stringify(query),this.childName); 98 | let pass = true; 99 | const keys=Object.keys(query); 100 | for (let k=0; k< Object.keys(query).length;k++) { 101 | const key=keys[k]; 102 | let condition = query[key]; 103 | if (key==this.childName) 104 | pass= this.evaluateCondition(item, condition['$elemMatch']); 105 | else if (key=='$or') 106 | pass = this.filterOr(item, condition); 107 | 108 | // console.log('key:', key, 'pass', pass); 109 | if (!pass) 110 | break; 111 | } 112 | return pass; 113 | 114 | } 115 | 116 | private evaluateCondition(i,condition) { 117 | let pass = true; 118 | // console.log(' evaluateCondition',i.seq, condition); 119 | const keys = Object.keys(condition); 120 | for (let k = 0; k < keys.length; k++) { 121 | const key = keys[k]; 122 | let cond = condition[key]; 123 | pass= this.evaluateValue(i,key,cond); 124 | if (pass===false) 125 | break; 126 | } 127 | return pass; 128 | } 129 | 130 | private evaluateValue(i,key,cond) { 131 | let pass = true; 132 | let val = i; 133 | if (key.includes('.')) { 134 | let ks = key.split('.'); 135 | ks.forEach(k => { 136 | val = val[k]; 137 | }); 138 | } 139 | else 140 | val=i[key]; 141 | 142 | if (typeof cond === 'object' && 143 | !Array.isArray(cond) && 144 | cond !== null) { 145 | pass = this.parseComplexCondition(cond, val); 146 | } 147 | else if (Array.isArray(val)) { 148 | if (Array.isArray(cond)) 149 | pass = val.some(r => cond.includes(r)) 150 | else if (!val.includes(cond)) 151 | pass = false; 152 | } 153 | else if (cond === null && val == null) 154 | pass = true; 155 | else if (val !== cond) 156 | pass = false; 157 | 158 | // console.log(' cond:', cond, key, i[key], pass); 159 | 160 | if (pass == false) 161 | return false; 162 | return pass; 163 | } 164 | 165 | private parseComplexCondition(condition,val) 166 | { 167 | let ret=false; 168 | Object.keys(condition).forEach(cond=>{ 169 | let term=condition[cond]; 170 | switch(cond) { 171 | case '$gte': 172 | ret=(val>term)||(val===term); 173 | break; 174 | case '$gt': 175 | ret=(val>term); 176 | break; 177 | case '$eq': 178 | ret=(val===term); 179 | break; 180 | case '$lte': 181 | ret=(val): any { 18 | 19 | if (Array.isArray(input)) 20 | { 21 | let inputRec={}; 22 | for(let i=0;i this.formatOutput(rule.outcomes)); // ✅ Respects order 59 | } 60 | 61 | return this.applyHitPolicy(matchedRules); 62 | } 63 | 64 | private applyHitPolicy(matchedRules: { outcomes: any[] }[]): any { 65 | const policy = this.table.hitPolicy; 66 | const outputColumns = this.table.outputColumns; 67 | 68 | if (policy === "COLLECT") { 69 | return matchedRules.map(rule => this.formatOutput(rule.outcomes)); 70 | } 71 | 72 | if (policy === "PRIORITY") { 73 | return this.formatOutput(matchedRules[0].outcomes); 74 | } 75 | 76 | if (policy === "RULE ORDER") { 77 | return matchedRules.map(rule => this.formatOutput(rule.outcomes)); 78 | } 79 | 80 | if (policy === "UNIQUE") { 81 | if (matchedRules.length !== 1) { 82 | throw new Error(`UNIQUE hit policy violated: ${matchedRules.length} rules matched.`); 83 | } 84 | return this.formatOutput(matchedRules[0].outcomes); 85 | } 86 | 87 | if (policy === "SUM") { 88 | return outputColumns.reduce((acc, col, index) => { 89 | acc[col.name] = matchedRules.reduce((sum, rule) => sum + (typeof rule.outcomes[index] === "number" ? rule.outcomes[index] : 0), 0); 90 | return acc; 91 | }, {} as Record); 92 | } 93 | 94 | if (policy === "MIN") { 95 | return outputColumns.reduce((acc, col, index) => { 96 | acc[col.name] = Math.min(...matchedRules.map(rule => rule.outcomes[index]).filter(v => typeof v === "number")); 97 | return acc; 98 | }, {} as Record); 99 | } 100 | 101 | if (policy === "COUNT") { 102 | return matchedRules.length; 103 | } 104 | 105 | return this.formatOutput(matchedRules[0].outcomes); 106 | } 107 | 108 | private matchesRule(ruleId,conditions: Condition[], input: Record): boolean { 109 | return conditions.every((condition, index) => { 110 | const inputColumn = this.table.inputColumns[index].name; 111 | 112 | let inputValue=null; 113 | if ((inputColumn in input)) 114 | inputValue=input[inputColumn]; 115 | 116 | return this.evaluateCondition(ruleId,inputValue, condition); 117 | }); 118 | } 119 | 120 | private evaluateCondition(ruleId,inputValue: T, condition: Condition): boolean { 121 | 122 | let ret; 123 | // check for Feel 124 | 125 | if (condition.operator==='feel'){ 126 | try { 127 | let ret=feelEvaluate(condition.value,inputValue); // Remove { } and parse FEEL 128 | console.log(` RuleId: ${ruleId} FEEL `,condition.value,'input:',inputValue,'ret',ret); 129 | return ret; 130 | } catch (error) { 131 | console.error("❌ FEEL Parsing Error:", error); 132 | } 133 | 134 | } 135 | 136 | 137 | ret =this.checkCondition(inputValue, condition.operator, condition.value); 138 | 139 | if (this.options.debug==true) { 140 | const match=(ret===true)?"🔍":"❌"; 141 | console.log(` ${match} Rule # ${ruleId} Checking: input: ${inputValue} oper: ${condition.operator} value: ${condition.value}`); 142 | 143 | } 144 | return ret; 145 | 146 | } 147 | private checkCondition(inputValue: T, operator: string, value: T | T[]): boolean { 148 | 149 | // Treat null or "-" as wildcard (always match) 150 | 151 | 152 | if (value === 'null' && (inputValue===null || inputValue=== undefined)) 153 | return true; 154 | if (value === null || value === "-" || value === undefined) { 155 | return true; 156 | } 157 | switch (operator) { 158 | case "==": return inputValue === value; 159 | case "!=": return inputValue !== value; 160 | case ">": return inputValue > value; 161 | case "<": return inputValue < value; 162 | case ">=": return inputValue >= value; 163 | case "<=": return inputValue <= value; 164 | default: 165 | console.log(`❌ Unknown operator: ${operator}`); 166 | return false; 167 | } 168 | } 169 | 170 | private formatOutput(outcomes: any[]): Record { 171 | return this.table.outputColumns.reduce((acc, col, index) => { 172 | acc[col.name] = outcomes[index]; 173 | return acc; 174 | }, {} as Record); 175 | } 176 | } 177 | 178 | export { DMNEngine } -------------------------------------------------------------------------------- /src/dmn/DMNParser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | 2️⃣ Core Features 4 | Feature Description 5 | DMN XML Parsing Load and parse DMN XML files into an internal model. 6 | Condition Evaluation Evaluate input data against conditions in the decision table. 7 | Hit Policy Handling Implement standard DMN hit policies (e.g., FIRST, COLLECT, PRIORITY, SUM). 8 | Multiple Input Columns Each rule row should match the corresponding input field automatically. 9 | Multiple Output Columns The decision table should return structured output based on defined columns. 10 | Data Type Handling Support numeric, boolean, string conditions. 11 | Performance Optimization Ensure fast decision evaluation for large tables. 12 | Logging & Debugging Track rule execution for debugging. 13 | Integration APIs Provide REST/CLI/SDK for calling the engine. 14 | 3️⃣ DMN Hit Policies to Support 15 | ✔ FIRST → Return the first matching rule. 16 | ✔ COLLECT → Return all matching rules. 17 | ✔ PRIORITY → Use the highest-priority match. 18 | ✔ RULE ORDER → Return matches in rule order. 19 | ✔ UNIQUE → Ensure exactly one match (error if multiple). 20 | ✔ SUM / MIN / MAX → Apply mathematical aggregation to numeric outputs. 21 | ✔ COUNT → Count how many rules match. 22 | 23 | 4️⃣ Inputs & Outputs 24 | Example DMN Table: 25 | 26 | Age Income Decision 27 | >=18 >50000 "Approved" 28 | <18 - "Rejected" 29 | ✔ Inputs: { age: 20, income: 60000 } 30 | ✔ Expected Output: { Decision: "Approved" } 31 | 32 | 33 | */ 34 | 35 | import * as fs from 'fs'; 36 | import * as xml2js from 'xml2js'; 37 | import { unaryTest, evaluate as feelEvaluate } from 'feelin'; 38 | 39 | /** 40 | * 41 | * 42 | ✔ Parses DMN XML into structured JSON. 43 | ✔ Extracts input/output columns dynamically. 44 | ✔ Processes DMN hit policies correctly. 45 | ✔ Handles encoded XML characters (<, >, &). 46 | */ 47 | 48 | class DMNParser { 49 | static async loadDMNFile(filePath: string) { 50 | const fileContent = fs.readFileSync(filePath, "utf-8"); 51 | const parser = new xml2js.Parser({ explicitArray: false }); 52 | 53 | try { 54 | const parsedXML = await parser.parseStringPromise(fileContent); 55 | return DMNParser.convertDMNToJSON(parsedXML); 56 | } catch (error) { 57 | console.error("Error parsing DMN XML:", error); 58 | throw new Error("Failed to parse DMN XML file."); 59 | } 60 | } 61 | 62 | static convertDMNToJSON(parsedXML: any): DecisionTable { 63 | const decisionTable = parsedXML.definitions.decision.decisionTable; 64 | const hitPolicy = decisionTable.$.hitPolicy; 65 | 66 | const inputColumns = decisionTable.input.map((input: any) => ({ 67 | 68 | name: DMNParser.getInputField(input).name, 69 | type: DMNParser.getInputField(input).type 70 | })); 71 | 72 | const outputColumns = decisionTable.output.map((output: any) => ({ 73 | name: DMNParser.getOutputField(output).name, 74 | type: DMNParser.getOutputField(output).type 75 | })); 76 | 77 | const rules = decisionTable.rule.map((rule: any) => ({ 78 | conditions: rule.inputEntry.map((entry: any, index: number) => ({ 79 | operator: DMNParser.extractOperator(entry.text), 80 | value: DMNParser.convertToType(entry.text, inputColumns[index].type) 81 | })), 82 | outcomes: rule.outputEntry.map((entry: any, index: number) => 83 | DMNParser.convertToType(entry.text.replace(/"/g, ""), outputColumns[index].type) 84 | ) 85 | })); 86 | 87 | return { hitPolicy, inputColumns, outputColumns, rules, defaultOutcome: {} }; 88 | } 89 | 90 | static extractOperator(conditionText: string): string { 91 | 92 | if (conditionText.startsWith("{") && conditionText.endsWith("}")) { 93 | try { 94 | let ret=feelEvaluate(conditionText.slice(1, -1)); // Remove { } and parse FEEL 95 | console.log('value is feel',conditionText,ret); 96 | return 'feel'; 97 | } catch (error) { 98 | console.error("❌ FEEL Parsing Error:", error); 99 | } 100 | } 101 | 102 | 103 | const match = conditionText.match(/(>=|<=|==|!=|>|<)/); 104 | return match ? match[0] : "=="; // Default to equality if no operator found 105 | } 106 | 107 | static convertToType(value: string, type: string): any { 108 | 109 | // Detect FEEL expressions wrapped in {} 110 | if (value.startsWith("{") && value.endsWith("}")) { 111 | return value.slice(1, -1); 112 | } 113 | 114 | const match = value.trim().match(/(>=|<=|==|!=|>|<)?\s*(-?\d+(\.\d+)?)/); 115 | 116 | if (match) { 117 | const numValue = match[2].includes(".") ? parseFloat(match[2]) : parseInt(match[2], 10); 118 | return numValue; 119 | } 120 | 121 | if (type === "boolean") return value.toLowerCase() === "true"; 122 | return value; 123 | } 124 | static getInputField(inpt) { 125 | /* 126 | 127 | 128 | income 129 | 130 | */ 131 | return {id:inpt.$.id,label:inpt.$.label,type:inpt.inputExpression.$.typeRef,name:inpt.$.label}; 132 | 133 | 134 | } 135 | static getOutputField(output) { 136 | /* 137 | 138 | 139 | */ 140 | return {id:output.$.id,name:output.$.name?output.$.name:output.$.label,type:output.$.typeRef}; 141 | 142 | } 143 | // New: Generate human-readable documentation of rules 144 | /** 145 | ✅ Summary 146 | ✅ Format rules in a human-readable structure. 147 | ✅ Include hit policy, input conditions, and output values. 148 | ✅ Display conditions per rule in a structured way. 149 | ✔ Generates clear, structured documentation of DMN decision rules. 150 | ✔ Formats conditions & outcomes properly. 151 | ✔ Displays hit policy, input columns, and output columns. 152 | ✔ Handles missing conditions gracefully (e.g., - for unrestricted conditions). 153 | **/ 154 | static documentRules(decisionTable) { 155 | let doc = `📄 **Decision Table Documentation**\n`; 156 | doc += `===================================\n`; 157 | doc += `🔹 **Hit Policy:** ${decisionTable.hitPolicy}\n`; 158 | doc += `🔹 **Input Columns:** ${decisionTable.inputColumns.map(col => col.name+":"+col.type).join(", ")}\n`; 159 | doc += `🔹 **Output Columns:** ${decisionTable.outputColumns.map(col => col.name+":"+col.type).join(", ")}\n\n`; 160 | doc += `📌 **Rules:**\n`; 161 | decisionTable.rules.forEach((rule, index) => { 162 | doc += `\n🔸 **Rule ${index + 1}:**\n`; 163 | doc += ` - **Conditions:**\n`; 164 | rule.conditions.forEach((condition, colIndex) => { 165 | doc += ` - ${decisionTable.inputColumns[colIndex].name} ${condition.operator} ${condition.value}\n`; 166 | }); 167 | doc += ` - **Outcomes:**\n`; 168 | rule.outcomes.forEach((outcome, colIndex) => { 169 | doc += ` - ${decisionTable.outputColumns[colIndex].name}: ${outcome}\n`; 170 | }); 171 | }); 172 | doc += `\n✅ **Default Outcome:**\n`; 173 | for (const [key, value] of Object.entries(decisionTable.defaultOutcome)) { 174 | doc += ` - ${key}: ${value}\n`; 175 | } 176 | return doc; 177 | } 178 | 179 | } 180 | 181 | // Define DecisionTable and Condition types 182 | type DecisionTable = { 183 | hitPolicy: string; 184 | inputColumns: {name: string; type: string}[]; 185 | outputColumns: {name: string; type: string}[]; 186 | rules: { conditions: Condition[]; outcomes: any[] }[]; 187 | defaultOutcome: Record; 188 | }; 189 | 190 | type Condition = { 191 | operator: string; //"==" | "!=" | ">" | "<" | ">=" | "<="; 192 | value: any; 193 | }; 194 | 195 | export { DMNParser , DecisionTable , Condition}; -------------------------------------------------------------------------------- /src/elements/Element.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Execution } from '../engine/Execution'; 3 | import { Token } from '../engine/Token'; 4 | import { IBehaviour, Behaviour} from './behaviours/.'; 5 | import { NODE_ACTION, FLOW_ACTION, EXECUTION_EVENT, TOKEN_STATUS, ITEM_STATUS } from '../'; 6 | 7 | import { Item } from '../engine/Item'; 8 | import { Node } from '.'; 9 | import { IElement } from '../interfaces/elements'; 10 | 11 | 12 | class Element implements IElement { 13 | id; 14 | type; 15 | subType; 16 | name; 17 | behaviours = new Map(); 18 | isFlow=false; 19 | lane; 20 | continue(item: Item) { } 21 | describe(): string[][] { 22 | return []; 23 | } 24 | restored(item: Item) { 25 | this.behaviours.forEach(behav => { behav.restored(item) }); 26 | } 27 | resume(item: Item) { 28 | this.behaviours.forEach(behav => { behav.resume(item) }); 29 | } 30 | /** 31 | * respond by providing behaviour attributes beyond item and node information 32 | * ex: timer due , input/outupt , fields 33 | * */ 34 | hasBehaviour(name): boolean { 35 | 36 | return this.behaviours.has(name); 37 | } 38 | getBehaviour(name) { 39 | return this.behaviours.get(name); 40 | } 41 | addBehaviour(nane,behavriour) { 42 | this.behaviours.set(nane,behavriour); 43 | } 44 | 45 | } 46 | 47 | export { Element} -------------------------------------------------------------------------------- /src/elements/Events.ts: -------------------------------------------------------------------------------- 1 | import { Node } from "."; 2 | import { Behaviour_names } from "./behaviours"; 3 | import { Loop, NODE_ACTION, TOKEN_TYPE } from "../"; 4 | import { Item } from "../engine/Item"; 5 | import { ITEM_STATUS, TOKEN_STATUS } from "../interfaces"; 6 | 7 | class Event extends Node { 8 | 9 | hasMessage() { 10 | return this.getBehaviour(Behaviour_names.MessageEventDefinition); 11 | } 12 | hasSignal() { 13 | return this.getBehaviour(Behaviour_names.SignalEventDefinition); 14 | } 15 | hasTimer() { 16 | return this.getBehaviour(Behaviour_names.TimerEventDefinition); 17 | } 18 | /** 19 | * 20 | * using token: check if fromEventBasedGateway; if yes cancel all other events 21 | * 22 | * @param item 23 | */ 24 | async start(item: Item): Promise { 25 | return super.start(item); 26 | } 27 | 28 | async end(item: Item,cancel: Boolean=false) { 29 | 30 | return await super.end(item,cancel); 31 | } 32 | get canBeInvoked() { return true; } 33 | 34 | /** 35 | * is called by 36 | * - boundaryEvent (intrupting) 37 | * - Error,Cancel Event 38 | * - End event (abort) 39 | * 40 | * 41 | * @param item the curremt event item 42 | */ 43 | static async terminate(item) { 44 | 45 | if (!item.token.parentToken) 46 | return; 47 | item.token.log('BoundaryEvent(' + item.node.id + ').run: isCancelling .. parentToken: '+item.token.parentToken.id); 48 | item.token.parentToken.currentItem.status = ITEM_STATUS.end; //force status so it would not run 49 | /* fix bug #86 50 | */ 51 | item.status = ITEM_STATUS.end; 52 | 53 | // check for loop: 54 | if (item.node.attachedTo.loopDefinition) 55 | { // cancel all items of the loop 56 | 57 | await Loop.cancel(item); 58 | } 59 | // 60 | let pToken=item.token.parentToken; // to be terminated 61 | 62 | if (pToken.type==TOKEN_TYPE.SubProcess && pToken.parentToken && pToken.parentToken.type==TOKEN_TYPE.Instance) 63 | await pToken.parentToken.terminate(); 64 | else 65 | await item.token.parentToken.terminate(); 66 | 67 | if (item.token.parentToken.originItem && item.token.parentToken.originItem.elementId==item.node.attachedTo.id) { // finding the attached item 68 | item.token.parentToken.originItem.node.end(item.token.parentToken.originItem,true); 69 | } 70 | 71 | } 72 | } 73 | 74 | class CatchEvent extends Event { 75 | 76 | get isCatching(): boolean { return true; } 77 | 78 | get requiresWait() { 79 | return true; // return this.hasMessage(); 80 | } 81 | get canBeInvoked() { 82 | return true; // return this.hasMessage(); 83 | } 84 | 85 | async start(item: Item): Promise { 86 | return super.start(item); 87 | } 88 | } 89 | class BoundaryEvent extends Event { 90 | 91 | get isCatching(): boolean { return true; } 92 | 93 | isCancelling: boolean; 94 | constructor(id, process, type, def) { 95 | super(id, process, type, def); 96 | 97 | this.isCancelling = true; 98 | if ((typeof this.def['cancelActivity'] !=='undefined') && (this.def['cancelActivity'] === false)) 99 | this.isCancelling=false; 100 | 101 | } 102 | get requiresWait() { 103 | return true; 104 | } 105 | get canBeInvoked() { 106 | return true; 107 | } 108 | 109 | async start(item: Item): Promise { 110 | return await super.start(item); 111 | } 112 | async run(item: Item): Promise { 113 | 114 | //if (item.token.parentToken && (item.token.parentToken.currentItem.status == ITEM_STATUS.end)) // in cancelling mode 115 | // return; why would I call run if am cancelling? 116 | var ret=await super.run(item); 117 | 118 | if (this.isCancelling) { 119 | 120 | item.token.status=TOKEN_STATUS.terminated; 121 | await Event.terminate(item); 122 | item.token.status=TOKEN_STATUS.wait; 123 | 124 | // current token is already terminated in the above logic, we need to restore it 125 | item.token.status=TOKEN_STATUS.running; 126 | } 127 | 128 | return ret; 129 | } 130 | } 131 | class ThrowEvent extends Event { 132 | 133 | /** 134 | * 135 | * using token: check if fromEventBasedGateway; if yes cancel all other events 136 | */ 137 | 138 | get isCatching(): boolean { return false; } 139 | 140 | async start(item: Item): Promise { 141 | return await super.start(item); 142 | } 143 | async run(item: Item): Promise { 144 | 145 | return NODE_ACTION.end; 146 | } 147 | } 148 | 149 | class EndEvent extends Event { 150 | 151 | get isCatching(): boolean { return false; } 152 | async end(item: Item,cancel) { 153 | 154 | let subProcessToken=item.token.getSubProcessToken(); 155 | if (subProcessToken && item.status !== ITEM_STATUS.end) 156 | { 157 | await subProcessToken.end(cancel); 158 | } 159 | 160 | return super.end(item,cancel); 161 | } 162 | } 163 | class StartEvent extends Event { 164 | constructor(id, process, type, def) { 165 | super(id, process, type, def); 166 | this.candidateGroups= this.def.$attrs["camunda:candidateGroups"]; 167 | this.candidateUsers = this.def.$attrs["camunda:candidateUsers"]; 168 | if (this.def.$attrs && this.def.$attrs["camunda:initiator"]) { 169 | this.initiator = this.def.$attrs["camunda:initiator"]; 170 | } 171 | } 172 | async start(item: Item): Promise { 173 | 174 | if (this.initiator) 175 | { 176 | item.token.data[this.initiator]=item.userName; 177 | } 178 | return await super.start(item); 179 | } 180 | 181 | get isCatching(): boolean { return true; } 182 | } 183 | 184 | export {Event,StartEvent, EndEvent , CatchEvent,ThrowEvent , BoundaryEvent} -------------------------------------------------------------------------------- /src/elements/Flow.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Execution } from '../engine/Execution'; 3 | import { Token, TOKEN_TYPE } from '../engine/Token'; 4 | import { IBehaviour, Behaviour} from "./behaviours"; 5 | import { NODE_ACTION, FLOW_ACTION, EXECUTION_EVENT, TOKEN_STATUS, ITEM_STATUS, IFlow, ScriptHandler } from '../'; 6 | 7 | import { Item } from '../engine/Item'; 8 | import { Node, Element } from '.'; 9 | import { IExecution } from '../interfaces'; 10 | 11 | class Flow extends Element implements IFlow { 12 | from: Node; 13 | to: Node; 14 | def; 15 | isMessageFlow = false; 16 | constructor(id, type, from, to, def) { 17 | super(); 18 | this.id = id; 19 | this.type = type; 20 | this.from = from; 21 | this.to = to; 22 | this.def = def; 23 | this.name = def.name; 24 | this.isFlow = true; 25 | } 26 | describe() { 27 | 28 | if (this.def.conditionExpression) { 29 | // conditionExpression:{"$type":"bpmn:Expression","body":"true"} 30 | let expression = this.def.conditionExpression.body; 31 | return [['condition: ' , expression]]; 32 | } 33 | else 34 | return []; 35 | } 36 | /** 37 | * ```xml 38 | * if flow has a condition, it must be evaluated and if result is true flow will continue 39 | * otherwise, flow will be discarded. 40 | * 41 | * ``` 42 | * @param item 43 | */ 44 | async run(item: Item) { 45 | item.token.log('Flow(' + this.name +'|'+ this.id + ').run: from='+this.from.name+' to=' + this.to.name + " find action... " ); 46 | let action = FLOW_ACTION.take; 47 | let result = await this.evaluateCondition(item); 48 | if (result !== true) { 49 | action = FLOW_ACTION.discard; 50 | item.token.execution.doItemEvent(item, EXECUTION_EVENT.flow_discard,{flow:this.id}); 51 | } 52 | else 53 | { 54 | item.token.execution.doItemEvent(item, EXECUTION_EVENT.flow_take,{flow:this.id}); 55 | item.token.info(`{"seq":${item.seq},"type":'${this.type}',"id":'${this.id}',"action":'Taken'}`); 56 | 57 | 58 | item.token.log('(Flow:'+this.id+')Flow(' + this.name +'|'+ this.id + ').run: going to ' + this.to.id + " action : " + action); 59 | } 60 | 61 | return action; 62 | } 63 | async end(item) { 64 | 65 | } 66 | async evaluateCondition(item) { 67 | // conditionExpression:{"$type":"bpmn:Expression","body":"true"} 68 | if (this.def.conditionExpression) { 69 | //console.log('flow definition ',this.def); 70 | let expression = this.def.conditionExpression.body; 71 | item.token.log('..conditionExpression:' + JSON.stringify(expression)); 72 | item.token.log(JSON.stringify(item.token.data)); 73 | let result = await item.context.scriptHandler.evaluateExpression(item, expression); 74 | item.token.log('..conditionExpression:' + expression + " result: " + result); 75 | return result; 76 | } 77 | return true; 78 | } 79 | async execute(item) { 80 | 81 | } 82 | } 83 | // --------------------------------------------- 84 | /** 85 | * ```xml 86 | * MessageFlow: can only be sent to active node in waiting 87 | * or to a start event 88 | * 89 | * ``` 90 | * */ 91 | class MessageFlow extends Flow { 92 | isMessageFlow = true; 93 | 94 | async execute(item: Item) { 95 | item.token.log('..MessageFlow -' + this.id + ' going to ' + this.to.id); 96 | 97 | const execution:IExecution = item.token.execution; 98 | let token = null; 99 | 100 | execution.tokens.forEach(t => { 101 | if (t.currentNode && t.currentNode.id == this.to.id) 102 | token = t; 103 | }); 104 | if (token) { 105 | item.token.log(' signalling token:' + token.id ); 106 | execution.promises.push(token.signal(null)); 107 | 108 | } 109 | else { 110 | item.token.log(' signalling new token:'); 111 | execution.promises.push(Token.startNewToken(TOKEN_TYPE.Primary,execution, this.to, null, null, null, null)); 112 | } 113 | } 114 | 115 | } 116 | export { Flow , MessageFlow} -------------------------------------------------------------------------------- /src/elements/NodeLoader.ts: -------------------------------------------------------------------------------- 1 | import { Node } from './'; 2 | import { BPMN_TYPE } from '../interfaces/Enums'; 3 | 4 | import { 5 | UserTask, ScriptTask, ServiceTask, SendTask, ReceiveTask, BusinessRuleTask, 6 | Gateway, EventBasedGateway, XORGateway , 7 | Event, CatchEvent, ThrowEvent, EndEvent , SubProcess, AdHocSubProcess,BoundaryEvent, CallActivity, StartEvent 8 | } from '.'; 9 | 10 | 11 | class NodeLoader { 12 | 13 | static loadNode(el, process): Node { 14 | 15 | switch (el.$type) { 16 | case BPMN_TYPE.UserTask: 17 | return new UserTask(el.id, process, el.$type, el); 18 | break; 19 | case BPMN_TYPE.ScriptTask: 20 | return new ScriptTask(el.id, process, el.$type, el); 21 | break; 22 | case BPMN_TYPE.ServiceTask: 23 | return new ServiceTask(el.id, process, el.$type, el); 24 | break; 25 | case BPMN_TYPE.BusinessRuleTask: 26 | return new BusinessRuleTask(el.id, process, el.$type, el); 27 | break; 28 | case BPMN_TYPE.SendTask: 29 | return new SendTask(el.id, process, el.$type, el); 30 | break; 31 | case BPMN_TYPE.ReceiveTask: 32 | return new ReceiveTask(el.id, process, el.$type, el); 33 | break; 34 | case BPMN_TYPE.SubProcess: 35 | return new SubProcess(el.id, process, el.$type, el); 36 | break; 37 | case BPMN_TYPE.AdHocSubProcess: 38 | return new AdHocSubProcess(el.id, process, el.$type, el); 39 | break; 40 | case BPMN_TYPE.ParallelGateway: 41 | return new Gateway(el.id, process, el.$type, el); 42 | break; 43 | case BPMN_TYPE.EventBasedGateway: 44 | return new EventBasedGateway(el.id, process, el.$type, el); 45 | break; 46 | case BPMN_TYPE.InclusiveGateway: 47 | return new Gateway(el.id, process, el.$type, el); 48 | break; 49 | case BPMN_TYPE.ExclusiveGateway: 50 | return new XORGateway(el.id, process, el.$type, el); 51 | break; 52 | case BPMN_TYPE.IntermediateCatchEvent: 53 | return new CatchEvent(el.id, process, el.$type, el); 54 | break; 55 | case BPMN_TYPE.IntermediateThrowEvent: 56 | return new ThrowEvent(el.id, process, el.$type, el); 57 | break; 58 | case BPMN_TYPE.BoundaryEvent: 59 | return new BoundaryEvent(el.id, process, el.$type, el); 60 | break; 61 | case BPMN_TYPE.EndEvent: 62 | return new EndEvent(el.id, process, el.$type, el); 63 | break; 64 | case BPMN_TYPE.StartEvent: 65 | return new StartEvent(el.id, process, el.$type, el); 66 | break; 67 | case BPMN_TYPE.CallActivity: 68 | return new CallActivity(el.id, process, el.$type, el); 69 | break; 70 | default: 71 | return new Node(el.id, process, el.$type, el); 72 | break; 73 | } 74 | } 75 | 76 | } 77 | 78 | export {NodeLoader , BPMN_TYPE} -------------------------------------------------------------------------------- /src/elements/Process.ts: -------------------------------------------------------------------------------- 1 | import { Execution } from '../engine/Execution'; 2 | import { Token, TOKEN_TYPE } from '../engine/Token'; 3 | import { ScriptHandler } from '../'; 4 | 5 | import { Node, Definition } from '.'; 6 | import { IExecution, NODE_SUBTYPE } from '../interfaces'; 7 | 8 | 9 | class Process { 10 | id; 11 | name; 12 | isExecutable; 13 | def: Definition; 14 | parent: Process; // Null for root process , value if subProcess 15 | childrenNodes: Node[]; 16 | eventSubProcesses: any[]; 17 | subProcessEvents: any[]; 18 | scripts = new Map(); 19 | candidateStarterGroups; 20 | candidateStarterUsers; 21 | historyTimeToLive; 22 | isStartableInTasklist; 23 | 24 | constructor(definition,parent=null) { 25 | this.id = definition.id; 26 | this.isExecutable = definition.isExecutable; 27 | this.name = definition.name; 28 | this.def = definition; 29 | this.parent = parent; 30 | this.candidateStarterGroups=definition.candidateStarterGroups; 31 | this.candidateStarterUsers=definition.candidateStarterUsers; 32 | this.historyTimeToLive=definition.historyTimeToLive; 33 | this.isStartableInTasklist=definition.isStartableInTasklist; 34 | 35 | } 36 | init(children, eventSubProcesses) { 37 | this.childrenNodes = children; 38 | this.eventSubProcesses = eventSubProcesses; 39 | } 40 | /** 41 | * Notify process that it started 42 | * */ 43 | async start(execution: Execution, parentToken) { 44 | this.doEvent(execution, 'start'); 45 | 46 | this.subProcessEvents = []; 47 | const events = []; 48 | this.eventSubProcesses.forEach(p => { 49 | p.getStartNodes().forEach(st => { 50 | events.push(st); 51 | }); 52 | }); 53 | let i; 54 | for (i = 0; i < events.length; i++) { 55 | const st = events[i]; 56 | execution.log('..starting event start subporcess ' + st.id) 57 | if (parentToken && parentToken.id==0) 58 | parentToken=null; 59 | const newToken = await Token.startNewToken(TOKEN_TYPE.EventSubProcess,execution, st, null, parentToken , null, null); 60 | this.subProcessEvents.push(newToken.currentItem); 61 | } 62 | } 63 | /** 64 | * Notify process that it ended 65 | * */ 66 | async end(execution:IExecution) { 67 | if (execution['ending']) 68 | return; 69 | execution['ending']=true; 70 | this.doEvent(execution, 'end'); 71 | let i; 72 | /* does not work because subProcessEvents is not saved 73 | for (i = 0; i < this.subProcessEvents.length;i++) { 74 | const event = this.subProcessEvents[i]; 75 | await event.token.terminate(); 76 | } */ 77 | let tks=[]; 78 | execution.tokens.forEach(tk=>{ 79 | if (tk.type==TOKEN_TYPE.EventSubProcess && tk.parentToken == null) 80 | tks.push(tk); 81 | }); 82 | for(i=0;i { 93 | if (node.type == 'bpmn:StartEvent') { 94 | if (!(userInvokable && ( 95 | (node.subType == NODE_SUBTYPE.timer) || 96 | (node.subType == NODE_SUBTYPE.error) || 97 | (node.subType == NODE_SUBTYPE.message) || 98 | (node.subType == NODE_SUBTYPE.signal)))) { 99 | 100 | starts.push(node); 101 | } 102 | } 103 | 104 | }) 105 | return starts; 106 | } 107 | getEventSubProcessStart(): Node[] { 108 | 109 | let starts = []; 110 | this.eventSubProcesses.forEach(sp => { 111 | 112 | const startNodes = sp.getStartNodes(); 113 | 114 | startNodes.forEach(n => { 115 | starts.push(n); 116 | }); 117 | }); 118 | return starts; 119 | } 120 | async doEvent(execution,event,eventDetails={}) { 121 | execution.log('Process(' + this.name + '|' + this.id + ').doEvent: executing script for event:' + event); 122 | const scripts = this.scripts.get(event); 123 | if (scripts) { 124 | for (var s = 0; s < scripts.length; s++) { 125 | var script = scripts[s]; 126 | const ret = await execution.scriptHandler.executeScript(execution, script); 127 | 128 | } 129 | } 130 | await execution.doExecutionEvent(this,event,eventDetails); 131 | } 132 | describe() { 133 | var desc = []; 134 | 135 | this.scripts.forEach((scripts, event) => { 136 | scripts.forEach(scr => { 137 | desc.push([`script on ${event} `, `${scr}`]); 138 | }); 139 | }) 140 | return desc; 141 | } 142 | 143 | } 144 | 145 | export { Process } -------------------------------------------------------------------------------- /src/elements/Transaction.ts: -------------------------------------------------------------------------------- 1 | import { Node } from './Node'; 2 | 3 | import { Token, TOKEN_TYPE } from '../engine/Token'; 4 | import { BPMN_TYPE, ITEM_STATUS, NODE_ACTION, NODE_SUBTYPE } from '../interfaces/Enums' 5 | 6 | import { Process } from './Process'; 7 | import { IExecution } from '../interfaces/engine'; 8 | import { EXECUTION_STATUS } from '../interfaces/Enums'; 9 | import { Item } from '../engine/Item'; 10 | import { SubProcess } from '.'; 11 | 12 | //NO_import { DecisionTable } from 'dmn-engine'; 13 | 14 | class Transaction extends SubProcess { 15 | get requiresWait() { return true; } 16 | 17 | async end(item,cancel:Boolean=false) { 18 | //console.log('trans ending'); 19 | super.end(item,cancel); 20 | 21 | } 22 | get isTransaction() { return true;} 23 | /** 24 | * Cancel Transaction 25 | * is called by Throw Cancel Event 26 | * 27 | * 1. Aborts any started items in the transaction 28 | * 2. Compensate any completed items 29 | * 30 | * @param item 31 | */ 32 | static async Cancel(transaction) { 33 | await Transaction.Compensate(transaction); 34 | 35 | } 36 | /** 37 | * Compensate Transaction 38 | * is called by Throw Compensate Event 39 | * this is called outside of the transaction 40 | * 41 | * 1. Compensate any completed items 42 | * 43 | * @param item 44 | */ 45 | static async Compensate(transItem) { 46 | 47 | try { 48 | 49 | if (transItem.node.isTransaction!==true) 50 | return; 51 | 52 | let trans = transItem.node as Transaction; 53 | let items = trans.getItems(transItem); 54 | 55 | 56 | for (let i = 0; i < items.length; i++) { 57 | let item: Item = items[i]; 58 | //console.log(" checking item ", item.elementId, item.status); 59 | if (item.status == ITEM_STATUS.end) { 60 | 61 | let evnts = item.node.attachments; 62 | let toFire = []; 63 | if (evnts) { 64 | evnts.forEach((event, key) => { 65 | //console.log(item.elementId, 'event', event.subType); 66 | if (event.subType == NODE_SUBTYPE.compensate) { 67 | //console.log("--- firing event", event.id); 68 | toFire.push(event); 69 | } 70 | }); 71 | for (let ev = 0; ev < toFire.length; ev++) { 72 | //console.log(ev); 73 | let newToken=await Token.startNewToken(TOKEN_TYPE.BoundaryEvent, item.token.execution, toFire[ev], null, item.token, item, null); 74 | //console.log('New Token', newToken.status, newToken.currentItem.id, newToken.currentItem.status); 75 | await newToken.execution.signalItem(newToken.currentItem.id,null); 76 | } 77 | 78 | } 79 | } 80 | } 81 | } 82 | catch(exc) { 83 | console.log(exc); 84 | } 85 | 86 | } 87 | getNodes() { 88 | return this.childProcess.childrenNodes; 89 | } 90 | 91 | getItemsForToken(token) { 92 | 93 | let items = []; 94 | token.childrenTokens.forEach(t => { 95 | t.path.forEach(it => { 96 | if (it.node.type !== BPMN_TYPE.SequenceFlow) 97 | items.push(it); 98 | }); 99 | items = items.concat(this.getItemsForToken(t)); 100 | }); 101 | 102 | return items; 103 | } 104 | 105 | public getItems(item) { 106 | 107 | return this.getItemsForToken(item.token); 108 | } 109 | 110 | async start(item): Promise { 111 | return super.start(item); 112 | } 113 | } 114 | 115 | 116 | export {Transaction } -------------------------------------------------------------------------------- /src/elements/behaviours/Behaviour.ts: -------------------------------------------------------------------------------- 1 | import { TimerBehaviour, CamundaFormData, IOBehaviour, MessageEventBehaviour, SignalEventBehaviour, TerminateBehaviour, LoopBehaviour } from "."; 2 | import type { Node } from ".."; 3 | import type { Item } from "../../engine/Item"; 4 | import { IItem } from "../../"; 5 | import { ScriptBehaviour } from "./Script"; 6 | 7 | 8 | const duration = require('iso8601-duration'); 9 | const parse = duration.parse; 10 | const end = duration.end; 11 | const toSeconds = duration.toSeconds; 12 | 13 | /** Behaviour 14 | * ioSpecification 15 | * timer 16 | * message 17 | * signal 18 | * 19 | * each behaviour is a class 20 | * it scans def and insert itself to perform actions as required 21 | * 22 | */ 23 | interface IBehaviour { 24 | node: Node; 25 | definition; 26 | start(item: IItem); 27 | run(item: IItem); 28 | end(item: IItem); 29 | restored(item: IItem); 30 | resume(item: IItem); 31 | getNodeAttributes(attributes: any[]); 32 | getItemAttributes(item: IItem, attributes: any[]); 33 | describe(): string[]; 34 | init(); 35 | } 36 | 37 | class Behaviour implements IBehaviour { 38 | node: Node; 39 | definition; 40 | constructor(node: Node,definition) { 41 | this.node = node; 42 | this.definition = definition; 43 | this.init(); 44 | } 45 | restored(item) { } 46 | describe() { return [];} 47 | init() {} 48 | enter(item: Item) { } 49 | start(item: Item) { } 50 | run(item: Item) { } 51 | end(item: Item) { } 52 | exit(item: Item) { } 53 | resume(item: Item) { } 54 | getNodeAttributes(attributes: any[]) {} 55 | getItemAttributes(item: Item, attributes: any[]) { } 56 | } 57 | 58 | 59 | export { IBehaviour, Behaviour} -------------------------------------------------------------------------------- /src/elements/behaviours/BehaviourLoader.ts: -------------------------------------------------------------------------------- 1 | import { TimerBehaviour, CamundaFormData, IOBehaviour, MessageEventBehaviour, SignalEventBehaviour, TerminateBehaviour, LoopBehaviour } from "."; 2 | import { Node } from "../Node"; 3 | import { Item } from "../../engine/Item"; 4 | import { IItem } from "../../"; 5 | import { ScriptBehaviour } from "./Script"; 6 | import { CancelEventBehaviour, CompensateEventBehaviour } from "./TransEvents"; 7 | import { EscalationEventBehaviour } from "./Escalation"; 8 | import { ErrorEventBehaviour } from './Error'; 9 | 10 | 11 | const duration = require('iso8601-duration'); 12 | const parse = duration.parse; 13 | const end = duration.end; 14 | const toSeconds = duration.toSeconds; 15 | 16 | const Behaviour_names = { 17 | TimerEventDefinition: 'bpmn:TimerEventDefinition', 18 | LoopCharacteristics: 'loopCharacteristics', 19 | IOSpecification: 'ioSpecification', 20 | TerminateEventDefinition: 'bpmn:TerminateEventDefinition', 21 | MessageEventDefinition: 'bpmn:MessageEventDefinition', 22 | SignalEventDefinition: 'bpmn:SignalEventDefinition', 23 | ErrorEventDefinition: 'bpmn:ErrorEventDefinition', 24 | EscalationEventDefinition: 'bpmn:EscalationEventDefinition', 25 | CancelEventDefinition: 'bpmn:CancelEventDefinition', 26 | CompensateEventDefinition: 'bpmn:CompensateEventDefinition', 27 | CamundaFormData: 'camunda:formData', 28 | CamundaScript: 'camunda:script', 29 | CamundaScript2: 'camunda:executionListener', 30 | CamundaScript3: 'camunda:taskListener', 31 | CamundaIO: 'camunda:inputOutput' 32 | 33 | } 34 | 35 | class BehaviourLoader { 36 | static behaviours = [ 37 | { 38 | name: Behaviour_names.TimerEventDefinition, funct: function (node, def) { 39 | return new TimerBehaviour(node, def); 40 | } 41 | }, 42 | { 43 | name: Behaviour_names.LoopCharacteristics, funct: function (node, def) { 44 | return new LoopBehaviour(node, def); 45 | } 46 | }, 47 | { 48 | name: Behaviour_names.CamundaFormData, funct: function (node, def) { 49 | return new CamundaFormData(node, def); 50 | } 51 | }, 52 | { 53 | name: Behaviour_names.CamundaIO, funct: function (node, def) { 54 | return new IOBehaviour(node, def); 55 | } 56 | }, 57 | { 58 | name: Behaviour_names.MessageEventDefinition, funct: function (node, def) { 59 | return new MessageEventBehaviour(node, def); 60 | } 61 | }, 62 | { 63 | name: Behaviour_names.SignalEventDefinition, funct: function (node, def) { 64 | return new SignalEventBehaviour(node, def); 65 | } 66 | }, 67 | { 68 | name: Behaviour_names.ErrorEventDefinition, funct: function (node, def) { 69 | 70 | return new ErrorEventBehaviour(node, def); 71 | } 72 | }, 73 | { 74 | name: Behaviour_names.EscalationEventDefinition, funct: function (node, def) { 75 | 76 | return new EscalationEventBehaviour(node, def); 77 | } 78 | }, 79 | { 80 | name: Behaviour_names.CompensateEventDefinition, funct: function (node, def) { 81 | 82 | return new CompensateEventBehaviour(node, def); 83 | } 84 | }, 85 | { 86 | name: Behaviour_names.CancelEventDefinition, funct: function (node, def) { 87 | 88 | return new CancelEventBehaviour(node, def); 89 | } 90 | } , 91 | 92 | 93 | { 94 | name: Behaviour_names.CamundaScript2, funct: function (node, def) { 95 | return new ScriptBehaviour(node, def); 96 | } 97 | }, 98 | { 99 | name: Behaviour_names.CamundaScript, funct: function (node, def) { 100 | return new ScriptBehaviour(node, def); 101 | } 102 | }, 103 | { 104 | name: Behaviour_names.CamundaScript3, funct: function (node, def) { 105 | return new ScriptBehaviour(node, def); 106 | } 107 | }, 108 | { 109 | name: Behaviour_names.TerminateEventDefinition, funct: function (node, def) { 110 | return new TerminateBehaviour(node, def); 111 | } 112 | } 113 | ]; 114 | static register(name,funct) { 115 | 116 | BehaviourLoader.behaviours.push({ name, funct }); 117 | } 118 | /** 119 | * #### 1. Load behaviours from node definition 120 | * 121 | * `node.definition[]` 122 | * 123 | * #### 2. Load behaviours from node definition.eventDefinitions 124 | * 125 | * ```ts 126 | * node.definition.eventDefinitions 127 | * $type == 128 | * ``` 129 | * example: 130 | * 131 | * ```xml 132 | 133 | PT2S 134 | 135 | * ``` 136 | * #### 3. Load behaviours from node definition.extensionElements 137 | * 138 | * ```ts 139 | * node.definitions.extensionElements 140 | * $type == 141 | * ``` 142 | * example: 143 | * 144 | * ```xml 145 | * 'camunda:formData' 146 | 147 | 148 | 149 | 150 | 151 | < /extensionElements> 152 | * ``` 153 | * 154 | * @param node 155 | */ 156 | static load(node: Node) { 157 | BehaviourLoader.behaviours.forEach(behav => { 158 | if (node.def[behav.name]) { 159 | node.addBehaviour(behav.name, behav.funct(node, node.def[behav.name])); 160 | } 161 | }); 162 | if (node.def.eventDefinitions) { 163 | node.def.eventDefinitions.forEach(ed => { 164 | BehaviourLoader.behaviours.forEach(behav => { 165 | if (ed.$type == behav.name) { 166 | node.addBehaviour(behav.name, behav.funct(node, ed)); 167 | } 168 | }); 169 | }); 170 | } 171 | if (node.def.extensionElements && node.def.extensionElements.values) { 172 | node.def.extensionElements.values.forEach(ext => { 173 | BehaviourLoader.behaviours.forEach(behav => { 174 | if (ext.$type == behav.name) { 175 | node.addBehaviour(behav.name, behav.funct(node, ext)); 176 | } 177 | }); 178 | }); 179 | } 180 | } 181 | } 182 | 183 | 184 | export { BehaviourLoader, Behaviour_names } -------------------------------------------------------------------------------- /src/elements/behaviours/Error.ts: -------------------------------------------------------------------------------- 1 | import type { TimerBehaviour } from "."; 2 | import type { Node } from ".."; 3 | import { Behaviour } from '.'; 4 | import type { Item } from "../../engine/Item"; 5 | import { Event, NODE_SUBTYPE, Transaction } from "../../"; 6 | import { BPMN_TYPE, NODE_ACTION, TOKEN_STATUS } from "../../interfaces"; 7 | 8 | 9 | class ErrorEventBehaviour extends Behaviour { 10 | init() { 11 | this.node.subType = NODE_SUBTYPE.error; 12 | 13 | } 14 | async run(item: Item) { 15 | 16 | //if (item.token.parentToken && (item.token.parentToken.currentItem.status == ITEM_STATUS.end)) // in cancelling mode 17 | // return; why would I call run if am cancelling? 18 | await Event.terminate(item); 19 | // current token is already terminated in the above logic, we need to restore it 20 | item.token.status=TOKEN_STATUS.running; 21 | 22 | } 23 | async start(item: Item) { 24 | item.log("staring an Error Events "+this.node.isCatching); 25 | if (this.node.isCatching) { 26 | return NODE_ACTION.wait; 27 | } 28 | else { // throw a message 29 | item.log("Error Event is throwing an error"); 30 | 31 | let transItem; 32 | 33 | if (item.token.originItem.type==BPMN_TYPE.Transaction) 34 | transItem=item.token.originItem; 35 | else 36 | transItem=item.token.parentToken.originItem; 37 | 38 | 39 | await item.token.processError(this.errorId,item); 40 | 41 | await Transaction.Cancel(transItem); 42 | 43 | transItem.token.status=TOKEN_STATUS.terminated; 44 | 45 | await transItem.node.end(transItem,true); 46 | 47 | await item.node.end(item,false); // mark me as properly ended 48 | 49 | return NODE_ACTION.error; 50 | } 51 | 52 | } 53 | 54 | get errorId() { 55 | let ref=this.definition['bpmn:errorRef'] || this.definition['errorRef']; 56 | if (ref) 57 | { 58 | return ref['errorCode']; 59 | } 60 | } 61 | describe() { 62 | if (this.node.isCatching) 63 | return [['Message', `catches message '${this.errorId}'`]]; 64 | else 65 | return [['Message', `throws message '${this.errorId}'`]]; 66 | } 67 | } 68 | 69 | export { ErrorEventBehaviour } -------------------------------------------------------------------------------- /src/elements/behaviours/Escalation.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | import { Behaviour } from '.'; 4 | import { Item } from "../../engine/Item"; 5 | import { NODE_SUBTYPE } from "../../"; 6 | import { NODE_ACTION } from "../../interfaces"; 7 | 8 | 9 | class EscalationEventBehaviour extends Behaviour { 10 | constructor(node,def) { 11 | super(node,def); 12 | 13 | } 14 | init() { 15 | this.node.subType = NODE_SUBTYPE.escalation; 16 | 17 | } 18 | async start(item: Item) { 19 | item.log("staring an Error Events "+this.node.isCatching); 20 | if (this.node.isCatching) { 21 | return NODE_ACTION.wait; 22 | } 23 | else { // throw a message 24 | item.log("Error Event is throwing an error"); 25 | 26 | await item.token.processEscalation(this.escalationId,item); 27 | return NODE_ACTION.continue; 28 | 29 | } 30 | 31 | } 32 | 33 | get escalationId() { 34 | let ref=this.definition['bpmn:escalationRef']; 35 | if (ref) 36 | { 37 | return ref['escalationCode']; 38 | } 39 | 40 | } 41 | describe() { 42 | if (this.node.isCatching) 43 | return [['Message', `catches message '${this.escalationId}'`]]; 44 | else 45 | return [['Message', `throws message '${this.escalationId}'`]]; 46 | } 47 | } 48 | export { EscalationEventBehaviour} -------------------------------------------------------------------------------- /src/elements/behaviours/Form.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '.'; 2 | /* 'camunda:formData' 3 | 4 | 5 | 6 | 7 | 8 | < /extensionElements> */ 9 | class CamundaFormData extends Behaviour { 10 | fields; 11 | init() { 12 | this.fields = []; 13 | this.definition.$children.forEach(f => { 14 | this.fields.push(f); 15 | }); 16 | } 17 | getFields() { return this.fields; } 18 | describe() { 19 | let desc = []; 20 | let fields = ''; 21 | this.fields.forEach(f => { 22 | desc.push(['Form Field', `id: ${f.id} name: ${f.label} type: ${ f.type }`]); 23 | //fields += `

field id:'${f.id}' name:'${f.label}' type: ${f.type} ` 24 | }); 25 | return desc;//['input fields', fields]; 26 | } 27 | } 28 | 29 | 30 | export { CamundaFormData } -------------------------------------------------------------------------------- /src/elements/behaviours/IOBehaviour.ts: -------------------------------------------------------------------------------- 1 | import { TimerBehaviour } from "."; 2 | import { Node } from ".."; 3 | import { Behaviour } from '.'; 4 | import type { Item } from "../../engine/Item"; 5 | import { ScriptHandler } from "../../engine/ScriptHandler"; 6 | 7 | /* 8 | * will prepare input at start 9 | * will prepare output at end 10 | * 11 | */ 12 | 13 | class IOParameter { 14 | type; 15 | name; 16 | subType; 17 | value; 18 | isInput() { return (this.type == 'camunda:inputParameter'); } 19 | isOutput() { return (this.type == 'camunda:outputParameter'); } 20 | constructor(ioObject) { 21 | 22 | this.type = ioObject['$type']; 23 | this.name = ioObject['name']; 24 | if (ioObject['$body']) 25 | this.value = ioObject['$body']; 26 | else if (ioObject['$children']) { 27 | let details = ioObject['$children']; 28 | details.forEach(detail => { 29 | this.subType = detail['$type']; 30 | if (this.subType == 'camunda:list') { 31 | if (detail['$children']) { 32 | this.value = []; 33 | detail['$children'].forEach(entry => { 34 | this.value.push(entry['$body']); 35 | }); 36 | } 37 | } 38 | else if (this.subType == 'camunda:map') { 39 | if (detail['$children']) { 40 | const map = new Map(); 41 | detail['$children'].forEach(entry => { 42 | map.set(entry['key'], entry['$body']); 43 | }); 44 | this.value = map; 45 | } 46 | } 47 | else if (this.subType == 'camunda:script') { 48 | this.value=detail['$body']; 49 | } 50 | else 51 | this.value = detail['$children']; 52 | }); 53 | } 54 | } 55 | /** 56 | * 57 | * 58 | #### Input/Output 59 | 60 | `myVar` Type| Example Value| | 61 | --- | --- | --- | 62 | Text/String| hello| No explicit quotes needed 63 | List| [ 'str1', 'str2', item.data.myExistingVar ] 64 | Map| { 'key1': 'val1', 'key2': 'val2' } 65 | JavaScript | data.item.myExistingVar 66 | JavaScript| { 'key1': 'val1', 'key2': 'val2' , 'key3': [item.data.myExistingVar, 'hello'], 'key4': { 'ikey5': item.data.myExistingVar}} 67 | 68 | * 69 | * @param item 70 | * 71 | * @returns 72 | */ 73 | async evaluate(item) { 74 | /** 75 | * scenario for call 76 | * */ 77 | var val; 78 | var evalValue; 79 | if (this.subType == 'camunda:text') { 80 | val = this.value; 81 | } 82 | else if (this.subType == 'camunda:list') { 83 | val = []; 84 | for(const entry of this.value) { 85 | //val.push(item.token.execution.appDelegate.scopeEval(item, entry)); 86 | 87 | evalValue = await item.context.scriptHandler.evaluateExpression(item, entry); 88 | 89 | val.push(evalValue); 90 | } 91 | } 92 | else if (this.subType == 'camunda:map') { 93 | val = new Map(); 94 | for (const [key,value] of this.value) { 95 | // (this.value).forEach(async (value, key) => { 96 | //const newVal = item.token.execution.appDelegate.scopeEval(item, value); 97 | evalValue = await item.context.scriptHandler.evaluateExpression(item, value); 98 | 99 | val.set(key, evalValue) 100 | } 101 | } 102 | else if (this.subType == 'camunda:script') { 103 | val = await item.context.scriptHandler.evaluateExpression(item, this.value); 104 | } 105 | else { // just text 106 | if ((this.value.startsWith('$'))) 107 | val = await item.context.scriptHandler.evaluateExpression(item, this.value.substring(1)); 108 | else 109 | val=this.value; 110 | } 111 | 112 | return val; 113 | } 114 | 115 | describe() { 116 | /** 117 | * scenario for call 118 | * */ 119 | var val; 120 | var evalValue; 121 | if (this.subType == 'camunda:text') { 122 | val = 'text:'+this.value; 123 | } 124 | else if (this.subType == 'camunda:list') { 125 | val = []; 126 | this.value.forEach(entry => { 127 | val.push(entry); 128 | }); 129 | val='list:'+JSON.stringify(val); 130 | } 131 | else if (this.subType == 'camunda:map') { 132 | val = new Map(); 133 | (this.value).forEach((value, key) => { 134 | val.set(key, value) 135 | }); 136 | val='map:'+Array.from(val.entries()); 137 | } 138 | else if (this.subType == 'camunda:script') { 139 | val = 'script:'+this.value; 140 | } 141 | else { // just text 142 | val='text:'+this.value; 143 | } 144 | 145 | return val; 146 | } 147 | 148 | 149 | 150 | } 151 | class IOBehaviour extends Behaviour { 152 | parameters: IOParameter[]; 153 | 154 | init() { 155 | this.parameters = []; 156 | var ios = this.definition['$children']; 157 | for (var i = 0; i < ios.length; i++) { 158 | var io = ios[i]; 159 | this.parameters.push(new IOParameter(io));//['$type'], io['name'], io['$body'])); 160 | } 161 | } 162 | /* 163 | * process input parameters here 164 | * 165 | * generate item.input 166 | * 167 | */ 168 | async enter(item: Item) { 169 | 170 | if (!item.input) 171 | item.input = {}; 172 | var hasInput = false; 173 | for (const param of this.parameters) { 174 | // this.parameters.forEach(async param => { 175 | if (param.isInput()) { 176 | /** 177 | * scenario for call 178 | * */ 179 | hasInput = true; 180 | var val; 181 | val = await param.evaluate(item); 182 | item.input[param.name] = val; 183 | item.log('...set at enter data input : input.' + param.name + ' = ' + val); 184 | } 185 | } 186 | if (hasInput == false) { 187 | /** 188 | * scenario for throw 189 | * */ 190 | for (const param of this.parameters) { 191 | // this.parameters.forEach(async param => { 192 | if (param.isOutput()) { 193 | var val = await item.context.scriptHandler.evaluateExpression(item, param.value); 194 | item.output[param.name] = val; 195 | item.log('...set at enter data output : output.' + param.name + ' = ' + val); 196 | } 197 | } 198 | } 199 | } 200 | process(item: Item) { 201 | 202 | } 203 | /* 204 | * process output parameters here 205 | * 206 | * value is an expression need to be evaluated 207 | * 208 | * moving output into data 209 | * 210 | */ 211 | async exit(item: Item) { 212 | for (const param of this.parameters) { 213 | if (param.isOutput()) { 214 | /** 215 | * scenario for call results 216 | * */ 217 | 218 | if (typeof param.value !== 'undefined' && param.value !== '') { 219 | var val = await item.context.scriptHandler.evaluateExpression(item, param.value); 220 | item.log('...set at exit data output : data.' + param.name + ' = ' + val); 221 | item.token.data[param.name] = val; 222 | } 223 | else 224 | { 225 | item.token.data[param.name] = item.output; 226 | item.log('...set at exit data output : data.' + param.name + ' = ' + item.output); 227 | } 228 | } 229 | } 230 | 231 | } 232 | describe() { 233 | var input = ''; 234 | var output = ''; 235 | this.parameters.forEach(param => { 236 | if (param.isOutput()) { 237 | output +='\n' + param.name +'='+param.value; 238 | } 239 | else 240 | input+='
'+ param.name + '=' + param.describe(); 241 | 242 | }); 243 | 244 | return [['Input', input],['output',output]]; 245 | } 246 | } 247 | 248 | export { IOBehaviour } -------------------------------------------------------------------------------- /src/elements/behaviours/Loop.ts: -------------------------------------------------------------------------------- 1 | import { TimerBehaviour } from "."; 2 | import { Node } from '..'; 3 | import {Behaviour } from "."; 4 | import { Item } from "../../engine/Item"; 5 | 6 | /** 7 | * 8 | * 1. sequential: 9 | * 10 | * 2. parallel 11 | * 12 | * 3. repeater 13 | * 14 | * 15 | */ 16 | class LoopBehaviour extends Behaviour { 17 | init() { 18 | } 19 | 20 | get collection() { 21 | if (this.isStandard()) 22 | return null; 23 | else if (this.node.def.loopCharacteristics.collection) 24 | return this.node.def.loopCharacteristics.collection; 25 | else { 26 | return this.node.def.loopCharacteristics.$attrs["camunda:collection"]; 27 | } 28 | } 29 | isStandard() { 30 | return (this.node.def.loopCharacteristics['$type'] =='bpmn:StandardLoopCharacteristics'); 31 | } 32 | 33 | isSequential() { 34 | if (this.node.def.loopCharacteristics.isSequential) 35 | return this.node.def.loopCharacteristics.isSequential; 36 | else 37 | return false; 38 | } 39 | describe() { 40 | if (this.isSequential()) 41 | return [['loop', `is a sequential loop based on '${this.collection}'`]]; 42 | else 43 | return [['loop', `is a parallel loop based on '${this.collection}'`]]; 44 | 45 | } 46 | 47 | } 48 | 49 | export { LoopBehaviour } -------------------------------------------------------------------------------- /src/elements/behaviours/MessageSignal.ts: -------------------------------------------------------------------------------- 1 | import type { TimerBehaviour } from "."; 2 | import type { Node } from ".."; 3 | import { Behaviour } from '.'; 4 | import { Item } from "../../engine/Item"; 5 | import { NODE_SUBTYPE } from "../../"; 6 | /** 7 | * 8 | * 9 | * it is part of the following: 10 | * 11 | * events 12 | * sendTask 13 | * receiveTask 14 | * */ 15 | 16 | class MessageEventBehaviour extends Behaviour { 17 | init() { 18 | this.node.messageId = this.messageId; 19 | this.node.subType = NODE_SUBTYPE.message; 20 | 21 | } 22 | async start(item: Item) { 23 | item.log("message event behaviour start"); 24 | if (this.node.isCatching) { 25 | item.messageId = this.messageId; 26 | } 27 | else { // throw a message 28 | const output = await this.node.getOutput(item); 29 | const matchingKey = item.context.messageMatchingKey; 30 | item.token.log(`.Throwing Message <${this.messageId}> - output: ${JSON.stringify(output)} - matching key : ${JSON.stringify(matchingKey)}`); 31 | await item.context.appDelegate.messageThrown(this.messageId,output, matchingKey, item); 32 | } 33 | 34 | } 35 | end(item: Item) { 36 | 37 | } 38 | get messageId() { 39 | if (this.definition['messageRef']) 40 | return this.definition['messageRef']['name']; 41 | } 42 | describe() { 43 | if (this.node.isCatching) 44 | return [['Message', `catches message '${this.messageId}'`]]; 45 | else 46 | return [['Message', `throws message '${this.messageId}'`]]; 47 | } 48 | } 49 | class SignalEventBehaviour extends Behaviour { 50 | init() { 51 | this.node.signalId = this.signalId; 52 | this.node.subType = NODE_SUBTYPE.signal; 53 | } 54 | async start(item: Item) { 55 | 56 | if (this.node.isCatching) { 57 | item.signalId = this.signalId; 58 | } 59 | else { // throw a message 60 | const output = await this.node.getOutput(item); 61 | const matchingKey = item.context.messageMatchingKey; 62 | item.token.log(`.Throwing Signal <${this.signalId}> - output: ${JSON.stringify(output)} - matching key : ${JSON.stringify(matchingKey)}`); 63 | item.context.appDelegate.signalThrown(this.signalId, output, matchingKey, item); 64 | } 65 | 66 | 67 | } 68 | end(item: Item) { 69 | } 70 | describe() { 71 | if (this.node.isCatching) 72 | return ['Signal', `catches signal '${this.signalId}'`]; 73 | else 74 | return ['Signal', `throws signal '${this.signalId}'`]; 75 | } 76 | get signalId() { 77 | if (this.definition['signalRef']) 78 | return this.definition['signalRef']['name']; 79 | } 80 | } 81 | 82 | export { MessageEventBehaviour , SignalEventBehaviour} -------------------------------------------------------------------------------- /src/elements/behaviours/Script.ts: -------------------------------------------------------------------------------- 1 | import { Behaviour } from '.'; 2 | import { Item } from "../../"; 3 | 4 | class ScriptBehaviour extends Behaviour { 5 | /* 6 | * old: 7 | * 8 | 9 | console.log("This is the start event"); 10 | 11 | 12 | 13 | 14 | New: 15 | 16 | 17 | 18 | 19 | script1 20 | 21 | 22 | 23 | 24 | script2 25 | 26 | 27 | 28 | 29 | */ 30 | scripts: string[] ; 31 | init() { 32 | this.scripts = []; 33 | var scrs = this.definition['$children']||[]; 34 | for (var i = 0; i < scrs.length; i++) { 35 | var scr = scrs[i]; 36 | this.scripts.push(scr.$body); 37 | this.node.scripts.set(this.definition.event, this.scripts); 38 | } 39 | } 40 | /* 41 | start(item: Item) { 42 | 43 | if ((!this.event) || (this.event == 'start')) 44 | this.executeScript(item); 45 | } 46 | run(item: Item) { 47 | 48 | if ((this.event) && (this.event == 'run')) 49 | this.executeScript(item); 50 | } 51 | end(item: Item) { 52 | 53 | if ((this.event) && (this.event == 'end')) 54 | this.executeScript(item); 55 | } 56 | resume(item: Item) { 57 | } 58 | executeScript(item) { 59 | item.token.log('invoking script call ' + " for " + item.id); 60 | item.token.execution.appDelegate.scopeJS(item, this.script); 61 | item.token.log('returned from script call ' + " for " + item.id); 62 | } */ 63 | describe() { 64 | return super.describe(); 65 | } 66 | } 67 | 68 | export { ScriptBehaviour } -------------------------------------------------------------------------------- /src/elements/behaviours/Terminate.ts: -------------------------------------------------------------------------------- 1 | import { TimerBehaviour } from "."; 2 | import { Node } from ".."; 3 | import { Behaviour } from '.'; 4 | import { Item } from "../../engine/Item"; 5 | 6 | /* 7 | * will terminate all active nodes as a result of terminate end event 8 | * 9 | */ 10 | class TerminateBehaviour extends Behaviour { 11 | start(item: Item) { } 12 | end(item: Item) { 13 | 14 | item.token.execution.tokens.forEach(tok => { 15 | tok.terminate(); 16 | }); 17 | } 18 | describe() { 19 | return [['','Terminates all active nodes']]; 20 | } 21 | } 22 | 23 | export { TerminateBehaviour } -------------------------------------------------------------------------------- /src/elements/behaviours/Timer.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Token, Execution, TOKEN_TYPE } from '../../engine'; 3 | import { Node } from '..'; 4 | import { NODE_ACTION, FLOW_ACTION, EXECUTION_EVENT, TOKEN_STATUS, ITEM_STATUS } from '../../'; 5 | import { Item ,ScriptHandler} from '../../engine'; 6 | import { BPMNServer } from '../../server'; 7 | import { Behaviour } from './'; 8 | import { Cron } from '../../server/Cron'; 9 | import { NODE_SUBTYPE } from '../../interfaces'; 10 | 11 | const dayjs = require('dayjs'); 12 | var relativeTime = require('dayjs/plugin/relativeTime') 13 | dayjs.extend(relativeTime) 14 | 15 | 16 | 17 | 18 | /* 19 | * will fire a timer at start and go into wait sleep 20 | * timer will later invoke the item when due 21 | * 22 | * 23 | * 24 | * 25 | 2011-03-11T12:13:14Z 26 | 27 | 28 | Example (interval lasting 10 days): 29 | 30 | 31 | P10D 32 | 33 | 34 | Time Cycle 35 | 36 | Example (3 repeating intervals, each lasting 10 hours): 37 | 38 | 39 | R3/PT10H 40 | 41 | 42 | 43 | PT2S 44 | 45 | 46 | 47 | Item Attributes: 48 | item.timeDue 49 | -- only for repeated timers -- 50 | 51 | item.timerCount - count of completed timers 52 | 53 | */ 54 | class TimerBehaviour extends Behaviour { 55 | duration; 56 | repeat=1; 57 | timeCycle; 58 | timeDate; 59 | init() { 60 | let def; 61 | this.node.subType = NODE_SUBTYPE.timer; 62 | 63 | this.node.def.eventDefinitions.forEach(ed => { 64 | 65 | if (ed.$type == 'bpmn:TimerEventDefinition') { 66 | if (ed.timeDuration) { 67 | this.duration = ed.timeDuration.body; 68 | } 69 | else if (ed.timeCycle) { 70 | this.timeCycle = ed.timeCycle.body; 71 | } 72 | else if (ed.timeDate) { 73 | this.timeDate = ed.timeDate.body; 74 | } 75 | else { 76 | // console.log("Error No timeDuration is defined in "+this.node.process.name+' node '+this.node.id); 77 | } 78 | } 79 | }); 80 | 81 | } 82 | describe() { 83 | let spec = ''; 84 | if (this.duration) 85 | spec = 'Duration:'+ this.duration; 86 | else if (this.timeCycle) 87 | spec = 'Cycle:'+ this.timeCycle; 88 | else if (this.timeDate) 89 | spec ='DateTime:'+ this.timeDate; 90 | 91 | 92 | return [['timer',spec]]; 93 | } 94 | /** 95 | * return the next time the timer is due 96 | * format is time format 97 | * @param timerModifier - for testing purposes configuration can alter the timer 98 | */ 99 | async timeDue(item,timerModifier=null) { 100 | 101 | let seconds; 102 | let timeDue; 103 | if (timerModifier) 104 | seconds = timerModifier/1000; 105 | else { 106 | if (this.duration) { 107 | 108 | if (this.duration.startsWith('$')) 109 | this.duration= await item.context.scriptHandler.evaluateExpression(item,this.duration); 110 | 111 | //seconds = toSeconds((parse(this.duration))); 112 | seconds= Cron.timeDue(this.duration, null); 113 | timeDue = new Date(); 114 | timeDue = dayjs(timeDue).add(seconds,'s').toDate(); 115 | } 116 | else if (this.timeCycle) { 117 | 118 | if (this.timeCycle.startsWith('$')) 119 | this.timeCycle= await item.context.scriptHandler.evaluateExpression(item,this.timeCycle); 120 | 121 | //seconds = toSeconds((parse(this.timeCycle))); 122 | seconds = Cron.timeDue(this.timeCycle, null); 123 | this.repeat = this.getRepeat(this.timeCycle); 124 | timeDue = new Date(); 125 | timeDue = dayjs(timeDue).add(seconds,'s').toDate(); 126 | } 127 | else if (this.timeDate) { 128 | let timeDate=this.timeDate; 129 | if (timeDate.startsWith('$')) { 130 | timeDate= await item.context.scriptHandler.evaluateExpression(item,timeDate); 131 | } 132 | timeDue=timeDate; 133 | } 134 | } 135 | return timeDue; 136 | } 137 | getRepeat(input) { 138 | if (input.startsWith('R')) { 139 | var l = input.indexOf('/'); 140 | if (l > 0) 141 | return input.substring(1, l); 142 | } 143 | return 1; 144 | } 145 | async start(item: Item) { 146 | 147 | if (item.node.type == "bpmn:StartEvent") 148 | return; 149 | item.token.log("..------timer running --- " ); 150 | await this.startTimer(item); 151 | item.timerCount = 0; 152 | 153 | return NODE_ACTION.wait; 154 | } 155 | 156 | async startTimer(item) { 157 | 158 | let timerModifier = null; 159 | const config = item.context.configuration; 160 | if (config.timers && config.timers.forceTimersDelay) { 161 | timerModifier = config.timers.forceTimersDelay; 162 | item.token.log("...Timer duration modified by the configuration to " + timerModifier); 163 | } 164 | 165 | item.timeDue = await this.timeDue(item,timerModifier); 166 | 167 | item.token.log("timer is set at " + item.timeDue + " - "+ new Date(item.timeDue).toISOString()); 168 | 169 | const seconds = ((new Date(item.timeDue)).getTime()/1000) - (new Date().getTime()/1000); 170 | 171 | item.log("..setting timer for " + seconds + " seconds for: "+item.id); 172 | setTimeout(this.expires.bind({ item, instanceId: item.token.execution.id, timer: this }), seconds * 1000); 173 | } 174 | async expires() { 175 | let item = this['item'] as unknown as Item; 176 | let timer = this['timer']; 177 | let instanceId= this['instanceId']; 178 | 179 | const exec=item.token.execution; 180 | item.token.log("Action:---timer Expired --- lock:"+exec.isLocked+' for '+item.id); 181 | if (item.status == ITEM_STATUS.wait) // just in case it was cancelled 182 | { 183 | //item.token.signal(null); 184 | 185 | if (exec.isLocked===true) 186 | exec.promises.push(exec.signalItem(item.id, {})); 187 | else 188 | await exec.server.engine.invoke({ "items.id": item.id }, null); 189 | 190 | } 191 | // check for repeat 192 | if (timer.timeCycle) { 193 | 194 | //console.log('repeating ',item.timerCount); 195 | 196 | if (timer.repeat > item.timerCount) { 197 | 198 | let resp=await exec.server.engine.startRepeatTimerEvent(instanceId, item,{}); 199 | //let newToken=await Token.startNewToken(TOKEN_TYPE.BoundaryEvent, item.token.execution, item.node, null, item.token, item, null); 200 | //let newItem = newToken.currentItem; 201 | //item.token.log('new token for timer repeat ' + item.timerCount + ' '+newItem.elementId); 202 | 203 | // item.timerCount++; 204 | 205 | //await timer.startTimer(new); 206 | } 207 | } 208 | } 209 | 210 | end(item: Item) { 211 | Cron.timerEnded(item); 212 | item.timeDue = undefined; 213 | } 214 | resume() { } 215 | } 216 | export { TimerBehaviour} -------------------------------------------------------------------------------- /src/elements/behaviours/TransEvents.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Behaviour } from '.'; 3 | import { Item } from "../../engine/Item"; 4 | import { NODE_SUBTYPE } from "../../"; 5 | import { ITEM_STATUS, NODE_ACTION, TOKEN_STATUS } from "../../interfaces"; 6 | import { Node } from "../Node"; 7 | import { Event, Transaction } from "../"; 8 | 9 | class CancelEventBehaviour extends Behaviour { 10 | init() { 11 | this.node.subType = NODE_SUBTYPE.cancel; 12 | 13 | } 14 | async run(item: Item) { 15 | 16 | await Event.terminate(item); 17 | // current token is already terminated in the above logic, we need to restore it 18 | item.status=ITEM_STATUS.wait; 19 | item.token.status=TOKEN_STATUS.running; 20 | 21 | } 22 | async start(item: Item) { 23 | item.log("staring an Cancel Event " + this.node.isCatching); 24 | if (this.node.isCatching) { 25 | return NODE_ACTION.wait; 26 | } 27 | else { // throw a message 28 | item.log("Cancel Event is throwing a TransactionCancel"); 29 | 30 | let transItem=item.token.parentToken.originItem; 31 | 32 | await item.token.processCancel(item); // find the event and invoke it 33 | 34 | await Transaction.Cancel(transItem); 35 | /*transItem.token.status=TOKEN_STATUS.terminated; 36 | 37 | await transItem.node.end(transItem,true);*/ 38 | 39 | return NODE_ACTION.error; 40 | } 41 | 42 | } 43 | 44 | describe() { 45 | return [['CancelTransaction', ``]]; 46 | } 47 | } 48 | class CompensateEventBehaviour extends Behaviour { 49 | init() { 50 | this.node.subType = NODE_SUBTYPE.compensate; 51 | 52 | } 53 | async start(item: Item) { 54 | item.log("staring an Error Events " + this.node.isCatching); 55 | if (this.node.isCatching) { 56 | return NODE_ACTION.continue; 57 | } 58 | else { // throw a message 59 | item.log("Compensate Event"); 60 | var nodeId = this.TransactionId; 61 | // challenge: find the item for a node, assuming there is only one item 62 | var transItem; 63 | item.token.execution.tokens.forEach(t => { 64 | t.path.forEach(i => { 65 | if (i.node.id == this.TransactionId) { 66 | transItem = i; 67 | //console.log(" transItem", transItem); 68 | } 69 | 70 | }); 71 | }); 72 | //console.log("--- calling Compensate"); 73 | await Transaction.Compensate(transItem); 74 | //console.log("---- called Compensate"); 75 | 76 | return NODE_ACTION.continue; 77 | } 78 | 79 | } 80 | 81 | get TransactionId() { 82 | if (this.definition['activityRef']) 83 | return this.definition['activityRef']['id']; 84 | } 85 | describe() { 86 | return [['Compensate', ``]]; 87 | } 88 | } 89 | 90 | 91 | export { CompensateEventBehaviour , CancelEventBehaviour} -------------------------------------------------------------------------------- /src/elements/behaviours/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Behaviour'; 2 | export * from './BehaviourLoader'; 3 | export * from './Form'; 4 | export * from './IOBehaviour'; 5 | export * from './MessageSignal'; 6 | export * from './Terminate'; 7 | export * from './Timer'; 8 | export * from './Loop'; 9 | export * from './Script'; 10 | 11 | -------------------------------------------------------------------------------- /src/elements/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Definition'; 2 | export * from './Element'; 3 | export * from './Node'; 4 | export * from './Flow'; 5 | export * from './NodeLoader'; 6 | export * from './Tasks'; 7 | export * from './Events'; 8 | export * from './behaviours/.'; 9 | export * from './Gateway'; 10 | export * from './Process'; 11 | export * from './Transaction'; -------------------------------------------------------------------------------- /src/elements/js-bpmn-moddle.ts: -------------------------------------------------------------------------------- 1 | const moddleOptions = 2 | { 3 | "name": "Node bpmn-engine", 4 | "uri": "http://paed01.github.io/bpmn-engine/schema/2017/08/bpmn", 5 | "prefix": "js", 6 | "xml": { 7 | "tagAlias": "lowerCase" 8 | }, 9 | "types": [ 10 | { 11 | "name": "Task", 12 | "isAbstract": true, 13 | "extends": ["bpmn:Task"], 14 | "properties": [ 15 | { 16 | "name": "result", 17 | "isAttr": true, 18 | "type": "String" 19 | }] 20 | }, 21 | { 22 | "name": "Output", 23 | "superClass": ["Element"] 24 | }, 25 | { 26 | "name": "Collectable", 27 | "isAbstract": true, 28 | "extends": ["bpmn:MultiInstanceLoopCharacteristics"], 29 | "properties": [ 30 | { 31 | "name": "collection", 32 | "isAttr": true, 33 | "type": "String" 34 | }, 35 | { 36 | "name": "elementVariable", 37 | "isAttr": true, 38 | "type": "String" 39 | } ] 40 | }, 41 | { 42 | "name": "FormSupported", 43 | "isAbstract": true, 44 | "extends": [ 45 | "bpmn:StartEvent", 46 | "bpmn:UserTask" 47 | ], 48 | "properties": [ 49 | { 50 | "name": "camunda:formKey", 51 | "isAttr": true, 52 | "type": "String" 53 | } 54 | ] 55 | }, 56 | { 57 | "name": "SendCall", 58 | "isAbstract": true, 59 | "extends": [ 60 | "bpmn:SendTask" 61 | ], 62 | "properties": [ 63 | { 64 | "name": "camunda:delegateExpression", 65 | "isAttr": true, 66 | "type": "String" 67 | } 68 | ] 69 | }, 70 | { 71 | "name": "ServiceCall", 72 | "isAbstract": true, 73 | "extends": [ 74 | "bpmn:ServiceTask" 75 | ], 76 | "properties": [ 77 | { 78 | "name": "camunda:delegateExpression", 79 | "isAttr": true, 80 | "type": "String" 81 | } 82 | ] 83 | }, 84 | { 85 | "name": "processExtensions", 86 | "isAbstract": true, 87 | "extends": [ 88 | "bpmn:Process" 89 | ], 90 | "properties": [ 91 | { 92 | "name": "camunda:candidateStarterGroups", 93 | "isAttr": true, 94 | "type": "String" 95 | }, 96 | { 97 | "name": "camunda:candidateStarterUsers", 98 | "isAttr": true, 99 | "type": "String" 100 | }, 101 | { 102 | "name": "camunda:historyTimeToLive", 103 | "isAttr": true, 104 | "type": "String" 105 | }, 106 | { 107 | "name": "camunda:isStartableInTasklist", 108 | "isAttr": true, 109 | "type": "Boolean", 110 | "default": true 111 | } 112 | ] 113 | } 114 | ] 115 | }; 116 | 117 | export {moddleOptions} -------------------------------------------------------------------------------- /src/engine/DataHandler.ts: -------------------------------------------------------------------------------- 1 | class DataHandler 2 | { 3 | // Data Handling 4 | /* 5 | * renamed from applyInput to appendData 6 | */ 7 | static appendData(instanceData,inputData,item, dataPath = null,assignment=null) { 8 | let asArray = false; 9 | 10 | if (Array.isArray(inputData)) 11 | asArray = true; 12 | 13 | if (dataPath && dataPath.endsWith('[]')) { 14 | asArray = true; 15 | } 16 | 17 | let target = DataHandler.getAndCreateData(instanceData,dataPath, asArray); 18 | 19 | if (!target) { 20 | item.token.error("*** Error *** target is not defined"); 21 | return; 22 | } 23 | 24 | if (inputData) { 25 | if (asArray) { 26 | target.push(inputData); 27 | } 28 | else { 29 | Object.keys(inputData).forEach(key => { 30 | const val = inputData[key]; 31 | if (key.startsWith('vars.')) { 32 | delete inputData[key]; 33 | if (item) 34 | item.vars[key.substring(5)] = val; 35 | } 36 | else 37 | target[key] = val; 38 | }); 39 | } 40 | } 41 | } 42 | 43 | static getData(instanceData,dataPath) { 44 | let target = instanceData; 45 | 46 | if (dataPath) { 47 | dataPath.split('.').forEach(de => { 48 | // strip off [] 49 | de=de.replace('[]', ''); 50 | if (de != '') 51 | target = target[de]; 52 | }); 53 | } 54 | return target; 55 | } 56 | static getAndCreateData(instanceData,dataPath, asArray = false) { 57 | 58 | let target = instanceData; 59 | 60 | if (dataPath) { 61 | dataPath.split('.').forEach(de => { 62 | if (de != '') { 63 | de = de.replace('[]', ''); 64 | if (!target[de]) { 65 | if (asArray) 66 | target[de] = []; 67 | else 68 | target[de] = {}; 69 | } 70 | target = target[de]; 71 | 72 | } 73 | }); 74 | } 75 | return target; 76 | } 77 | } 78 | 79 | export { DataHandler } -------------------------------------------------------------------------------- /src/engine/DefaultAppDelegate.ts: -------------------------------------------------------------------------------- 1 | import { IExecution, Item, IAppDelegate, IServiceProvider} from "../"; 2 | import { moddleOptions} from '../elements/js-bpmn-moddle'; 3 | 4 | class DefaultAppDelegate implements IAppDelegate { 5 | server; 6 | 7 | 8 | constructor(server) { 9 | this.server = server; 10 | let self = this; 11 | server.listener.on('all', async function ({ context, event }) { 12 | await self.executionEvent(context, event); 13 | }); 14 | } 15 | 16 | async getServicesProvider(context): Promise { 17 | return this as unknown as IServiceProvider; 18 | } 19 | 20 | startUp(options) { 21 | console.log('server started..'); 22 | } 23 | 24 | sendEmail(to, msg, body) { 25 | throw Error("sendEmail must be implemented by AppDelegate"); 26 | } 27 | 28 | get moddleOptions() { 29 | return moddleOptions; 30 | } 31 | 32 | async executionStarted(execution: IExecution) { 33 | 34 | } 35 | 36 | async executionEvent(context,event) { 37 | 38 | } 39 | 40 | /** 41 | * is called when a event throws a message 42 | * 43 | * @param messageId 44 | * @param data 45 | * @param messageMatchingKey 46 | * @param item 47 | */ 48 | async messageThrown(messageId, data, messageMatchingKey: any , item: Item) { 49 | const msgId = item.node.messageId; 50 | item.log("Message Issued" + msgId); 51 | // issue it back for others to receive 52 | const resp = await item.context.engine.throwMessage(msgId, data, messageMatchingKey); 53 | if (resp && resp.instance) { 54 | item.log(" invoked another process " + resp.instance.id + " for " + resp.instance.name); 55 | } 56 | else 57 | await this.issueMessage(messageId, data); 58 | } 59 | /** 60 | * 61 | * is called when an event throws a message that can not be answered by another process 62 | * 63 | * @param messageId 64 | * @param data 65 | */ 66 | async issueMessage(messageId, data) { 67 | 68 | } 69 | async issueSignal(signalId, data) { 70 | 71 | } 72 | async signalThrown(signalId, data, messageMatchingKey: any, item: Item) { 73 | 74 | item.log("Signal Issued" + signalId); 75 | // issue it back for others to receive 76 | 77 | const resp = await item.context.engine.throwSignal(signalId, data, messageMatchingKey); 78 | if (resp && resp.instance) { 79 | item.log(" invoked another process " + resp.instance.id + " for " + resp.instance.name); 80 | } 81 | else 82 | await this.issueSignal(signalId, data); 83 | } 84 | async serviceCalled(serviceName, data, item: Item) { 85 | item.log("Service called:"+serviceName+data); 86 | 87 | } 88 | 89 | } 90 | 91 | 92 | async function delay(time, result) { 93 | console.log("delaying ... " + time) 94 | return new Promise(function (resolve) { 95 | setTimeout(function () { 96 | console.log("delayed is done."); 97 | resolve(result); 98 | }, time); 99 | }); 100 | } 101 | export { DefaultAppDelegate } 102 | 103 | 104 | -------------------------------------------------------------------------------- /src/engine/Item.ts: -------------------------------------------------------------------------------- 1 | import { Execution } from "./Execution"; 2 | import { ITEM_STATUS, IItem, } from "../"; 3 | import { IItemData } from "../"; 4 | import { Element , Node } from '../elements'; 5 | import { Token } from "./Token"; 6 | 7 | class Item implements IItem { 8 | id; 9 | itemKey: string; // Used for multi-instance 10 | element: Element; 11 | token : Token; 12 | seq; 13 | userName; 14 | startedAt; // dateTime Started 15 | endedAt = null; 16 | instanceId; 17 | input = {}; 18 | output = {}; 19 | vars = {}; 20 | assignee; 21 | candidateGroups; 22 | candidateUsers; 23 | dueDate; 24 | followUpDate; 25 | priority; 26 | status: ITEM_STATUS; 27 | statusDetails:object; 28 | log(...msg) { return this.token.log(msg); } 29 | get data() { return this.token.data; } 30 | set data(val) { this.token.appendData(val,this); } 31 | setData(val) { this.token.appendData(val,this); } 32 | get options() { return this.token.execution.options; } 33 | get context() { return this.token.execution; } 34 | get elementId() { return this.element.id; } 35 | get name() { 36 | return this.element.name; 37 | } 38 | get tokenId() { 39 | return this.token.id; 40 | } 41 | get type() { 42 | return this.element.type; 43 | } 44 | get node() : Node { 45 | return this.element as Node; 46 | } 47 | // timer 48 | timeDue: Date; 49 | timerCount; 50 | 51 | messageId; 52 | signalId; 53 | 54 | constructor(element, token, status = ITEM_STATUS.start) { 55 | this.id = token.execution.getUUID(); 56 | this.seq = token.execution.getNewId('item'); 57 | 58 | this.messageId= element.messageId; 59 | this.signalId=element.signalId; 60 | 61 | this.element = element; 62 | this.token = token; 63 | this.status = status; 64 | this.userName = token.execution.userName; 65 | 66 | this.itemKey=token.itemsKey;//token.dataPath; 67 | 68 | 69 | } 70 | save() : IItemData { 71 | 72 | return { 73 | id: this.id, seq: this.seq, itemKey: this.itemKey, tokenId: this.token.id, elementId: this.elementId, name: this.name, 74 | status: this.status, statusDetails: this.statusDetails, userName: this.userName, startedAt: this.startedAt, endedAt: this.endedAt, type: this.type, timeDue: this.timeDue, 75 | /*data: null ,*/ vars: this.vars, output:this.output, instanceId: this.instanceId, 76 | messageId: this.messageId, signalId: this.signalId, 77 | assignee:this.assignee, 78 | candidateGroups:this.candidateGroups, 79 | candidateUsers:this.candidateUsers, 80 | dueDate:this.dueDate, 81 | followUpDate:this.followUpDate, 82 | priority:this.priority 83 | }; 84 | 85 | } 86 | static load(execution: Execution, dataObject: IItemData, token) { 87 | const el = execution.getNodeById(dataObject.elementId); 88 | const item = new Item(el, token, dataObject.status); 89 | item.id = dataObject.id; 90 | item.itemKey = dataObject.itemKey; 91 | item.seq = dataObject.seq; 92 | item.userName = dataObject.userName; 93 | item.startedAt = dataObject.startedAt; 94 | item.endedAt = dataObject.endedAt; 95 | item.timeDue = dataObject.timeDue; 96 | item.statusDetails = dataObject.statusDetails; 97 | item.messageId= dataObject.messageId; 98 | item.signalId= dataObject.signalId; 99 | item.assignee= dataObject.assignee; 100 | item.candidateGroups=dataObject.candidateGroups; 101 | item.candidateUsers=dataObject.candidateUsers; 102 | item.dueDate=dataObject.dueDate; 103 | item.followUpDate=dataObject.followUpDate; 104 | item.priority=dataObject.priority; 105 | item.vars = dataObject.vars; 106 | item.output=dataObject.output; 107 | return item; 108 | } 109 | } 110 | 111 | export {Item} -------------------------------------------------------------------------------- /src/engine/Model.ts: -------------------------------------------------------------------------------- 1 | import { IItemData, IInstanceData} from '../' 2 | import { ITEM_STATUS , EXECUTION_STATUS } from '../'; 3 | import { BPMN_TYPE } from '../elements'; 4 | 5 | /** 6 | * as stored in MongoDB 7 | * */ 8 | class InstanceObject implements IInstanceData { 9 | id; 10 | name; 11 | status : EXECUTION_STATUS; 12 | version=null; 13 | startedAt; 14 | endedAt; 15 | saved; 16 | data; 17 | items=[]; 18 | source; 19 | logs=[]; 20 | tokens=[]; 21 | loops=[]; 22 | parentItemId; 23 | vars; 24 | 25 | } 26 | /** 27 | * as stroed in MongoDB 28 | * */ 29 | class ItemObject implements IItemData { 30 | id; 31 | seq; 32 | itemKey; 33 | name; 34 | type : BPMN_TYPE; 35 | status: ITEM_STATUS; 36 | userName; 37 | startedAt; 38 | endedAt; 39 | // derived 40 | tokenId; 41 | elementId; 42 | /** 43 | * retrieved from findObjects 44 | * */ 45 | instanceId; 46 | processName; 47 | timeDue: Date; 48 | data; 49 | vars; 50 | output; 51 | messageId; 52 | signalId; 53 | assignee; 54 | candidateGroups; 55 | candidateUsers; 56 | dueDate; 57 | followUpDate; 58 | priority; 59 | 60 | } 61 | class TokenObject { 62 | id; 63 | status; 64 | dataPath; 65 | loopId; 66 | parentTokenId; 67 | branchNodeId; 68 | startNodeId; 69 | currentNodeId; 70 | } 71 | 72 | class LoopObject { 73 | nodeId; 74 | ownerTokenId; 75 | dataPath; 76 | sequence; 77 | } 78 | 79 | // Query Objects 80 | 81 | class DataQuery { 82 | instance; 83 | item; 84 | 85 | constructor(instance,item) { 86 | 87 | } 88 | } 89 | function test() { 90 | // new DataQuery(new InstanceQuery()) 91 | } 92 | class Query { 93 | private _instanceId; 94 | private _instanceName; 95 | private _instanceStatus: EXECUTION_STATUS; 96 | private _instanceStartedAt; 97 | private _instanceEndedAt; 98 | private _instanceSaved; 99 | private _instanceData; 100 | private _instanceSource; 101 | private _instanceParentNodeId; 102 | private _data; 103 | private _itemId; 104 | private _itemSeq; 105 | private _itemKey; 106 | private _itemName; 107 | private _itemType: BPMN_TYPE; 108 | private _itemStatus: ITEM_STATUS; 109 | private _itemStartedAt; 110 | private _itemEndedAt; 111 | private _itemTokenId; 112 | private _itemElementId; 113 | 114 | constructor({ instanceId = null, 115 | instanceName = null, 116 | instanceStatus = null, 117 | instanceStartedAt = null, 118 | instanceEndedAt = null, 119 | instanceSaved = null, 120 | data = null, 121 | itemId=null, 122 | itemSeq=null, 123 | itemKey=null, 124 | itemName=null, 125 | itemType= null, 126 | itemStatus=null, 127 | itemStartedAt=null, 128 | itemEndedAt=null, 129 | itemTokenId=null, 130 | itemElementId=null 131 | 132 | }: { 133 | instanceId?, instanceName?, instanceStatus?: EXECUTION_STATUS, instanceStartedAt?, instanceEndedAt?, instanceSaved?, data? 134 | itemId?,itemSeq?,itemKey?,itemName?, itemType?: BPMN_TYPE, itemStatus?: ITEM_STATUS,itemStartedAt?,itemEndedAt?, 135 | itemTokenId?, itemElementId? 136 | } = {}) { 137 | 138 | this._instanceId = instanceId; 139 | this._instanceName = instanceName; 140 | this._instanceStatus = instanceStatus; 141 | this._instanceStartedAt = instanceStartedAt; 142 | this._instanceEndedAt = instanceEndedAt; 143 | this._instanceSaved = instanceSaved; 144 | this._data = data; 145 | this._itemId = itemId; 146 | this._itemSeq = itemSeq; 147 | this._itemKey = itemKey; 148 | this._itemName = itemName; 149 | this._itemType = itemType; 150 | this._itemStatus = itemStatus; 151 | this._itemStartedAt =itemStartedAt ; 152 | this._itemEndedAt = itemEndedAt; 153 | this._itemTokenId = itemTokenId; 154 | this._itemElementId = itemElementId; 155 | } 156 | instanceId(val): Query { this._instanceId = val; return this; } 157 | instanceName(val): Query { this._instanceName = val; return this; } 158 | instanceStatus(val: EXECUTION_STATUS): Query { this._instanceStatus = val; return this; } 159 | data(val) { this._data = val; return this;} 160 | itemId(val): Query { this._itemId = val; return this; } 161 | itemName(val): Query { this._itemName = val; return this; } 162 | itemStatus(val:ITEM_STATUS): Query { this._itemStatus = val; return this; } 163 | itemElementId(val): Query { this._itemElementId = val; return this; } 164 | } 165 | 166 | class InstanceQuery 167 | { 168 | _id; 169 | _name; 170 | _status: EXECUTION_STATUS; 171 | _startedAt; 172 | _endedAt; 173 | _saved; 174 | _data; 175 | items : ItemQuery = new ItemQuery(); 176 | _source; 177 | _parentNodeId; 178 | 179 | constructor({ id = null, 180 | name = null, 181 | status = null, 182 | startedAt = null, 183 | endedAt = null, 184 | saved = null, 185 | data = null, 186 | items = new ItemQuery() 187 | }: { id?, name?, status?: EXECUTION_STATUS , startedAt?, endedAt?, saved?,data?,items?: ItemQuery} = {}) 188 | { 189 | this._id = id; 190 | this._name = name; 191 | this._status = status; 192 | } 193 | id(val):InstanceQuery { this._id = val; return this;} 194 | name(val): InstanceQuery { this._name = val; return this; } 195 | } 196 | class ItemQuery { 197 | _id; 198 | _seq; 199 | _itemKey; 200 | _name; 201 | _type: BPMN_TYPE; 202 | _status: ITEM_STATUS; 203 | _startedAt; 204 | _endedAt; 205 | _tokenId; 206 | _elementId; 207 | constructor({ id = null, 208 | name = null, 209 | seq = null, 210 | status = null, 211 | itemKey = null, 212 | type = null, 213 | startedAt = null, 214 | endedAt = null, 215 | tokenId = null, 216 | elementId = null, 217 | 218 | }: { id?, name?, seq?, status?: ITEM_STATUS, itemKey?, type?: BPMN_TYPE, startedAt?, endedAt?, tokenId?, elementId?} = {}) { 219 | this._id = id; 220 | this._seq = seq; 221 | this._itemKey = itemKey; 222 | this._name = name; 223 | this._type = type; 224 | this._status = status; 225 | this._startedAt = startedAt; 226 | this._endedAt = endedAt; 227 | this._tokenId = tokenId; 228 | this._elementId = elementId; 229 | 230 | } 231 | id(val) { this._id = val; return this;} 232 | name(val) { this._name = val; return this;} 233 | status(val) { this._status = val; return this;} 234 | 235 | } 236 | 237 | 238 | 239 | export {Query, InstanceObject , ItemObject, TokenObject , LoopObject} -------------------------------------------------------------------------------- /src/engine/ScriptHandler.ts: -------------------------------------------------------------------------------- 1 | import { Item ,Execution,Token} from "."; 2 | import { IScriptHandler } from "../interfaces"; 3 | import { spawn } from 'child_process'; 4 | 5 | 6 | class ScriptHandler implements IScriptHandler{ 7 | 8 | 9 | /** 10 | ** these expression are strings with $ 11 | * scenarios: 12 | * String =>as is 13 | * $javaScript =>evaulated 14 | * a , b , c =>array 15 | * dateString =>convert to date 16 | * Examples: 17 | * ['T', `user1`], 18 | ['T', `$(appServices.test1(100))`], 19 | ['T', `$(appServices.getSupervisorUser('user1'))`], 20 | ['T', `abc,xyz,user group`], 21 | ['TD', `2022-10-11`], 22 | ** 23 | * 24 | * appDelegate.scopeEval -->evaluateExpression 25 | * appDelegate.scopeJS -->executeScript 26 | * 27 | */ 28 | async evaluateInputExpression(item, exp, dateFormat = false) { 29 | 30 | if (!exp) 31 | return; 32 | var val; 33 | 34 | if (exp.startsWith('$')) { 35 | val =await this.evaluateExpression(item, exp); 36 | } 37 | else if (exp.includes(",")) { 38 | const arr = exp.split(","); 39 | val = arr; 40 | } 41 | else 42 | val = exp; 43 | 44 | if (dateFormat) 45 | val = new Date(val); 46 | 47 | return val; 48 | //console.log('----setAttVal', attr, exp, val); 49 | } 50 | // old name :scopeEval(scope, script) { 51 | /** 52 | * execute JavaScript expression , no need for $ 53 | * 54 | * @param scope 55 | * @param expression 56 | * @returns 57 | */ 58 | async evaluateExpression(scope: Item|Token, expression) { 59 | 60 | let script=expression; 61 | let result; 62 | let ret; 63 | 64 | if (!expression) 65 | return; 66 | if ((expression.startsWith('$'))) 67 | script=expression.substring(1); 68 | 69 | try { 70 | var js = ScriptHandler.getJSvars(scope) + ` 71 | return (${script});`; 72 | 73 | result = await Function(js).bind(scope)(); 74 | 75 | 76 | if (result instanceof Promise) 77 | { 78 | ret = await result; 79 | } 80 | else 81 | ret =result; 82 | } 83 | catch (exc) { 84 | console.log('error in script evaluation', js); 85 | console.log(exc); 86 | throw new Error(exc); 87 | } 88 | return ret; 89 | } 90 | // used to be called scopeJS 91 | async executeScript(scope: Item|Execution, script) { 92 | 93 | let result; 94 | let ret; 95 | 96 | /* old 97 | try { 98 | let result; 99 | var js = this.getJSvars(scope) + ` 100 | ${script};`; 101 | 102 | const func=new AsyncFunction(js); 103 | result = await func.bind(scope)(); 104 | } 105 | catch (exc) { 106 | console.log('error in script execution', js); 107 | console.log(exc); 108 | } 109 | 110 | return result; 111 | */ 112 | try { 113 | if (script.startsWith('$py')) 114 | { 115 | result=await this.runPython(scope,script.substring(3)); 116 | console.log('python result:',result) 117 | return result; 118 | } 119 | script = script.replace('#','') //remove symbol '#' 120 | //require return 121 | var js = ScriptHandler.getJSvars(scope) + ` 122 | ${script};`; 123 | result = await Function(js).bind(scope)(); 124 | 125 | if (result instanceof Promise) 126 | { 127 | ret = await result; 128 | //console.log(result,ret); 129 | } 130 | else 131 | ret =result; 132 | } 133 | catch (exc) { 134 | console.log('error in script execution', js); 135 | console.log(exc); 136 | throw new Error(exc); 137 | } 138 | 139 | return ret; 140 | } 141 | static getJSvars(scope) { 142 | let isToken = scope.hasOwnProperty('startNodeId'); 143 | let isExecution = scope.hasOwnProperty('tokens'); 144 | 145 | if (isToken) { 146 | return ` 147 | var data=this.data; 148 | var instance=this.execution.instance; 149 | var input=this.input; 150 | var output=this.output; 151 | var appDelegate=this.execution.appDelegate; 152 | var appServices=this.execution.servicesProvider; 153 | var appUtils=appDelegate.appUtils; 154 | var item=this; // for backward support only 155 | `; 156 | 157 | } 158 | else if (isExecution) { 159 | return ` 160 | var appDelegate=this.appDelegate; 161 | var instance=this.instance; 162 | var appServices=this.servicesProvider; 163 | var appUtils=appDelegate.appUtils; 164 | `; 165 | 166 | } 167 | else { 168 | return ` 169 | var item=this; 170 | var data=this.data; 171 | var instance=this.token.execution.instance; 172 | var input=this.input; 173 | var output=this.output; 174 | var appDelegate=this.token.execution.appDelegate; 175 | var appServices=this.token.execution.servicesProvider; 176 | var appUtils=appDelegate.appUtils; 177 | `; 178 | 179 | } 180 | } 181 | 182 | async runPython(item,code: string, input: any={}): Promise { 183 | return new Promise((resolve, reject) => { 184 | 185 | const pythonCmd = process.env.PYTHON_CMD||'python'; 186 | item.data['a']=3; 187 | item.data['b']=4; 188 | const pyCode=` 189 | import sys, json 190 | input = json.loads(sys.stdin.read()) 191 | data =${JSON.stringify(item.data)} 192 | item =${JSON.stringify({id:item.id,name:item.name,elementId:item.elementId})} 193 | result = data["a"] + data["b"] 194 | print(json.dumps({ "sum": result }), flush=True) 195 | ${code.trim()}`; 196 | const python = spawn(pythonCmd, ['-c', pyCode]); 197 | 198 | let output = ''; 199 | let error = ''; 200 | 201 | // Handle stdout 202 | python.stdout.on('data', (data) => { 203 | output += data.toString(); 204 | }); 205 | 206 | // Handle stderr 207 | python.stderr.on('data', (data) => { 208 | error += data.toString(); 209 | }); 210 | 211 | // Handle close 212 | python.on('close', (code) => { 213 | if (code !== 0 || error) { 214 | reject(new Error(`Python error: ${error || 'Exit code ' + code}`)); 215 | } else { 216 | try { 217 | const parsed = JSON.parse(output); 218 | resolve(parsed); 219 | } catch (e) { 220 | resolve(output.trim()); // fallback: plain string 221 | } 222 | } 223 | }); 224 | 225 | // Send input if provided 226 | if (input !== undefined) { 227 | python.stdin.write(JSON.stringify(input)); 228 | python.stdin.end(); 229 | } 230 | }); 231 | } 232 | } 233 | 234 | export {ScriptHandler} 235 | -------------------------------------------------------------------------------- /src/engine/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Execution'; 2 | export * from './Token'; 3 | export * from './Item'; 4 | export * from './Loop'; 5 | export * from './DefaultAppDelegate'; 6 | export * from './Model'; 7 | export * from './ScriptHandler'; 8 | export * from './DataHandler'; -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common/'; 2 | export * from './engine/'; 3 | export * from './elements/'; 4 | export * from './server/'; 5 | export * from './datastore/'; 6 | export * from './interfaces/'; 7 | export * from './API/'; 8 | -------------------------------------------------------------------------------- /src/interfaces/DataObjects.ts: -------------------------------------------------------------------------------- 1 | import { ITEM_STATUS, } from './Enums'; 2 | 3 | interface IItemData { 4 | id: string; // System generated unique Id 5 | itemKey: string; // application assigned key to call the item by 6 | elementId: string; // bpmn element 7 | name: string; // name of bpmn element 8 | type: string; // bpmn element type 9 | instanceId: string; // Instance Id of the item 10 | processName?: string; 11 | tokenId: any; // execution Token 12 | userName: any; 13 | startedAt: any; 14 | endedAt: any; 15 | seq: any; 16 | timeDue: Date; 17 | status: ITEM_STATUS; 18 | statusDetails?:object; 19 | data?: any; 20 | messageId; 21 | signalId; 22 | vars; 23 | output; 24 | assignee; 25 | candidateGroups; 26 | candidateUsers; 27 | dueDate; 28 | followUpDate; 29 | priority; 30 | 31 | } 32 | interface IInstanceData { 33 | id; 34 | name; 35 | status; 36 | version; 37 | startedAt; 38 | endedAt; 39 | saved; 40 | data; 41 | items; 42 | source; 43 | logs; 44 | tokens; 45 | loops; 46 | parentItemId; // used for subProcess Calls 47 | } 48 | 49 | 50 | 51 | interface IDefinitionData { 52 | name: any; 53 | processes: Map; 54 | rootElements: any; 55 | nodes: Map; 56 | flows: any[]; 57 | source: any; 58 | logger: any; 59 | accessRules: any[]; 60 | } 61 | 62 | 63 | interface IElementData { 64 | id: any; 65 | type: any; 66 | name: any; 67 | behaviours: Map; 68 | } 69 | 70 | interface IFlowData { 71 | 72 | } 73 | 74 | interface IEventData { 75 | elementId: string; 76 | processId: string; 77 | type; 78 | name; 79 | subType; 80 | signalId?: string; 81 | messageId?: string; 82 | // timer info 83 | expression; 84 | expressionFormat; // cron/iso 85 | referenceDateTime; // start time of event or last time timer ran 86 | maxRepeat; 87 | repeatCount; 88 | timeDue?: Date; 89 | lane?: string; 90 | candidateGroups?; 91 | candidateUsers?; 92 | 93 | } 94 | interface IBpmnModelData { 95 | name; 96 | source; 97 | svg; 98 | processes: IProcessData[]; 99 | events: IEventData[]; 100 | saved; 101 | // parse(definition: IDefinition); 102 | } 103 | interface IProcessData { 104 | id; 105 | name; 106 | isExecutable; 107 | candidateStarterGroups; 108 | candidateStarterUsers; 109 | historyTimeToLive; 110 | isStartableInTasklist; 111 | } 112 | export { IItemData, IInstanceData , IDefinitionData, IElementData, IFlowData , IBpmnModelData, IProcessData, IEventData } -------------------------------------------------------------------------------- /src/interfaces/DataObjects.ts.bak: -------------------------------------------------------------------------------- 1 | import { ITEM_STATUS, } from './Enums'; 2 | 3 | interface IItemData { 4 | id: string; // System generated unique Id 5 | itemKey: string; // application assigned key to call the item by 6 | elementId: string; // bpmn element 7 | name: string; // name of bpmn element 8 | type: string; // bpmn element type 9 | instanceId: string; // Instance Id of the item 10 | processName?: string; 11 | tokenId: any; // execution Token 12 | userName: any; 13 | startedAt: any; 14 | endedAt: any; 15 | seq: any; 16 | timeDue: Date; 17 | status: ITEM_STATUS; 18 | data?: any; 19 | messageId; 20 | signalId; 21 | vars; 22 | assignee; 23 | candidateGroups; 24 | candidateUsers; 25 | dueDate; 26 | followUpDate; 27 | priority; 28 | 29 | } 30 | interface IInstanceData { 31 | id; 32 | name; 33 | status; 34 | version; 35 | startedAt; 36 | endedAt; 37 | saved; 38 | data; 39 | items; 40 | source; 41 | logs; 42 | tokens; 43 | loops; 44 | parentItemId; // used for subProcess Calls 45 | } 46 | 47 | 48 | 49 | interface IDefinitionData { 50 | name: any; 51 | processes: Map; 52 | rootElements: any; 53 | nodes: Map; 54 | flows: any[]; 55 | source: any; 56 | logger: any; 57 | accessRules: any[]; 58 | } 59 | 60 | 61 | interface IElementData { 62 | id: any; 63 | type: any; 64 | name: any; 65 | behaviours: Map; 66 | } 67 | 68 | interface IFlowData { 69 | 70 | } 71 | 72 | interface IEventData { 73 | elementId: string; 74 | processId: string; 75 | type; 76 | name; 77 | subType; 78 | signalId?: string; 79 | messageId?: string; 80 | // timer info 81 | expression; 82 | expressionFormat; // cron/iso 83 | referenceDateTime; // start time of event or last time timer ran 84 | maxRepeat; 85 | repeatCount; 86 | timeDue?: Date; 87 | lane?: string; 88 | candidateGroups?; 89 | candidateUsers?; 90 | 91 | } 92 | interface IBpmnModelData { 93 | name; 94 | source; 95 | svg; 96 | processes: IProcessData[]; 97 | events: IEventData[]; 98 | saved; 99 | // parse(definition: IDefinition); 100 | } 101 | interface IProcessData { 102 | id; 103 | name; 104 | isExecutable; 105 | } 106 | export { IItemData, IInstanceData , IDefinitionData, IElementData, IFlowData , IBpmnModelData, IProcessData, IEventData } -------------------------------------------------------------------------------- /src/interfaces/Enums.ts: -------------------------------------------------------------------------------- 1 | import { Transaction } from "../elements"; 2 | 3 | enum BPMN_TYPE { 4 | UserTask = 'bpmn:UserTask', 5 | ScriptTask = 'bpmn:ScriptTask', 6 | ServiceTask = 'bpmn:ServiceTask', 7 | SendTask = 'bpmn:SendTask', 8 | ReceiveTask = 'bpmn:ReceiveTask', 9 | BusinessRuleTask = 'bpmn:BusinessRuleTask', 10 | SubProcess = 'bpmn:SubProcess', 11 | AdHocSubProcess ='bpmn:AdHocSubProcess', 12 | ParallelGateway = 'bpmn:ParallelGateway', 13 | EventBasedGateway = 'bpmn:EventBasedGateway', 14 | InclusiveGateway = 'bpmn:InclusiveGateway', 15 | ExclusiveGateway = 'bpmn:ExclusiveGateway', 16 | BoundaryEvent = 'bpmn:BoundaryEvent', 17 | StartEvent = 'bpmn:StartEvent', 18 | IntermediateCatchEvent = 'bpmn:IntermediateCatchEvent', 19 | IntermediateThrowEvent = 'bpmn:IntermediateThrowEvent', 20 | EndEvent = 'bpmn:EndEvent', 21 | SequenceFlow = 'bpmn:SequenceFlow', 22 | MessageFlow = 'bpmn:MessageFlow', 23 | CallActivity = 'bpmn:CallActivity', 24 | Transaction = 'bpmn:Transaction' 25 | } 26 | 27 | enum NODE_SUBTYPE { 28 | timer = 'timer', 29 | message = 'message', 30 | signal = 'signal', 31 | error = 'error', 32 | escalation = 'escalation', 33 | cancel = 'cancel', 34 | 35 | compensate = 'compensate' 36 | } 37 | /* 38 | * ALL events 39 | */ 40 | enum EXECUTION_EVENT { 41 | node_enter = 'enter', 42 | node_assign = 'assign', 43 | node_validate = 'validate', 44 | node_start = 'start', 45 | node_wait = 'wait', 46 | node_end = 'end', 47 | node_terminated = 'terminated', 48 | transform_input = 'transformInput', 49 | transform_output = 'transformOutput', 50 | flow_take = 'take', 51 | flow_discard = 'discard', 52 | process_loaded ='process.loaded', 53 | process_start = 'process.start', 54 | process_started = 'process.started', 55 | process_invoke = 'process.invoke', 56 | process_invoked = 'process.invoked', 57 | process_saving = 'process.saving', 58 | process_restored = 'process.restored', 59 | process_resumed = 'process_resumed', 60 | process_wait = 'process.wait', 61 | process_end = 'process.end', 62 | process_terminated = 'process.terminated', 63 | process_exception = 'process.exception', 64 | token_start = 'token.start', 65 | token_wait = 'token.wait', 66 | token_end = 'token.end', 67 | token_terminated = 'token.terminated', 68 | process_error = 'process.error' 69 | } 70 | /* 71 | * possible actions by node 72 | */ 73 | 74 | enum NODE_ACTION { continue = 1, wait, end , cancel, stop , error , abort }; 75 | 76 | enum ITEM_STATUS { 77 | enter = 'enter', 78 | start = 'start', 79 | wait = 'wait', 80 | end = 'end', 81 | terminated = 'terminated', 82 | cancelled = 'cancelled', 83 | discard = 'discard' 84 | 85 | } 86 | 87 | //type ITEMSTATUS = 'enter' | 'start' | 'wait' | 'end' | 'terminated' | 'discard'; 88 | 89 | enum EXECUTION_STATUS { running='running',wait='wait', end = 'end' , terminated ='terminated' } 90 | 91 | enum TOKEN_STATUS { running = 'running', wait = 'wait', end = 'end', terminated = 'terminated' , queued = 'queued' } 92 | /* 93 | * possible actions by flow 94 | */ 95 | // must be same as above 96 | 97 | enum FLOW_ACTION { take = 'take', discard = 'discard' } 98 | 99 | export { 100 | BPMN_TYPE , 101 | EXECUTION_EVENT, NODE_ACTION, FLOW_ACTION, 102 | ITEM_STATUS, TOKEN_STATUS, EXECUTION_STATUS , NODE_SUBTYPE 103 | } -------------------------------------------------------------------------------- /src/interfaces/User.ts: -------------------------------------------------------------------------------- 1 | 2 | interface IUserInfo { 3 | userName:string; // Unique, saved in the workflow 4 | userGroups:string[]; // to filter for security 5 | tenantId?:string; // Used to mark instances 6 | modelsOwner?:string; // Used If models are not shared among tentants 7 | } 8 | interface ISecureUser extends IUserInfo { 9 | 10 | isAdmin(): boolean; 11 | isSystem(): boolean; 12 | inGroup(userGroup): boolean; 13 | /** 14 | * alters the query adding conditions based on security rules 15 | * @param query 16 | * @returns query 17 | */ 18 | qualifyInstances(query); 19 | 20 | /** 21 | * alters the query adding conditions based on security rules 22 | * @param query 23 | * @returns query 24 | */ 25 | qualifyItems(query) 26 | /** 27 | * alters the query adding conditions based on security rules 28 | * @param query 29 | * @returns query 30 | */ 31 | 32 | qualifyStartEvents(query); 33 | /** 34 | * alters the query adding conditions based on security rules 35 | * @param query 36 | * @returns query 37 | */ 38 | qualifyDeleteInstances(query); 39 | /** 40 | * alters the query adding conditions based on security rules 41 | * @param query 42 | * @returns query 43 | */ 44 | qualifyModels(query); 45 | /** 46 | */ 47 | canModifyModel(name); 48 | /** 49 | */ 50 | canDeleteModel(name); 51 | /** 52 | * alters the query adding conditions based on security rules 53 | * @param query 54 | * @returns query 55 | */ 56 | qualifyViewItems(query); 57 | canInvoke(item); 58 | canAssign(item); 59 | canStart(name, startNodeId, user); 60 | } 61 | 62 | interface IUserService { 63 | findUsers(query); 64 | addUser(userName, email, password, userGroups); 65 | setPassword(userName, password); 66 | install(); 67 | } 68 | export { IUserService,IUserInfo,ISecureUser} -------------------------------------------------------------------------------- /src/interfaces/common.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IExecution, IItem, NODE_ACTION, FLOW_ACTION, IModelsDatastore, 3 | IDataStore, ICacheManager, 4 | IScriptHandler 5 | } from '../'; 6 | 7 | 8 | interface IConfiguration 9 | { 10 | definitionsPath?: string, 11 | templatesPath?: string, 12 | timers: { 13 | forceTimersDelay: number, 14 | precision: number, 15 | }, 16 | database: IMongoDBDatabaseConfiguration | ISQLiteDatabaseConfiguration, 17 | apiKey: string, 18 | logger: ILogger, 19 | definitions(server): IModelsDatastore, 20 | appDelegate(server): IAppDelegate, 21 | dataStore(server): IDataStore, 22 | scriptHandler(server): IScriptHandler, 23 | cacheManager(server): ICacheManager 24 | } 25 | 26 | interface IDatabaseConfigurationBase { 27 | loopbackRepositories?:any 28 | } 29 | interface IMongoDBDatabaseConfiguration extends IDatabaseConfigurationBase { 30 | MongoDB: { 31 | db_url: string, 32 | db: string, 33 | Locks_collection:string, 34 | Instance_collection:string, 35 | Archive_collection:string 36 | } 37 | } 38 | interface ISQLiteDatabaseConfiguration extends IDatabaseConfigurationBase { 39 | SQLite: { 40 | db_connection: string, 41 | }, 42 | } 43 | 44 | 45 | /** 46 | * A logging tool to take various message for monitoring and debugging 47 | * 48 | * it can also keep the message in memory till saved later through saveToFile 49 | * msgs can be cleared by the clean method 50 | * 51 | * */ 52 | interface ILogger { 53 | /** 54 | * 55 | * @param toConsole boolean 56 | * writes to the output console 57 | * @param toFile string 58 | * writes to file 59 | * 60 | */ 61 | 62 | setOptions({ toConsole, toFile, callback }: { 63 | toConsole: any; 64 | toFile: any; 65 | callback: any; 66 | }): void; 67 | clear(): void; 68 | get(): any[]; 69 | debug(...message: any): void; 70 | warn(...message: any): void; 71 | log(...message: any): void; 72 | error(err: any): void; 73 | reportError(err: any): void; 74 | save(filename: any): Promise; 75 | saveForInstance(instanceId:string); 76 | } 77 | 78 | /** 79 | * Object to respond to all named services 80 | */ 81 | interface IServiceProvider { 82 | [serviceName: string]: CallableFunction | IServiceProvider; 83 | } 84 | 85 | /** 86 | * Application Delegate Object to respond to various events and services: 87 | * 88 | * 1. receive all events from workflow 89 | * 2. receive service calls 90 | * 3. receive message and signal calls 91 | * 4. execute scripts 92 | * 93 | * */ 94 | interface IAppDelegate { 95 | moddleOptions; 96 | /** 97 | * Get the service task handlers, default to `this`, so you can add handlers on this class directly. 98 | */ 99 | getServicesProvider(execution: IExecution): IServiceProvider | Promise; 100 | sendEmail(to, msg, body); 101 | executionStarted(execution: IExecution); 102 | startUp(options); // start of server 103 | messageThrown(signalId: string, data, messageMatchingKey: any, item: IItem); 104 | signalThrown(signalId: string, data, messageMatchingKey: any, item: IItem); 105 | /** 106 | * 107 | * is called when an event throws a message that can not be answered by another process 108 | * 109 | * @param messageId 110 | * @param data 111 | */ 112 | issueMessage(messageId: string, data); 113 | issueSignal(messageId: string, data); 114 | /** 115 | * is called only if the serviceTask has no implementation; otherwise the specified implementation will be called. 116 | * 117 | * @param item 118 | */ 119 | serviceCalled(input: Record, execution: IExecution, item: IItem): unknown; 120 | } 121 | 122 | export { ILogger, IAppDelegate, IConfiguration, IServiceProvider } 123 | -------------------------------------------------------------------------------- /src/interfaces/datastore.ts: -------------------------------------------------------------------------------- 1 | import { IDefinition, IInstanceData } from './'; 2 | import { IBpmnModelData, IItemData, IEventData } from './'; 3 | 4 | export interface FindParams { 5 | filter?: Record; 6 | after?: string; 7 | limit?: number; 8 | sort?: Record; 9 | projection?: Record; 10 | lastItem?: Record; 11 | latestItem?: Record; 12 | getTotalCount?: boolean; // if true, return total count of items in the result set 13 | } 14 | 15 | export interface FindResult { 16 | data?: any[]; 17 | nextCursor?: string | null; 18 | totalCount?: number; 19 | error?: string; 20 | } 21 | 22 | interface IDataStore { 23 | dbConfiguration: any; 24 | db: any; 25 | logger: any; 26 | locker: any; 27 | save(instance:any,options:any): Promise; 28 | loadInstance(instanceId: any,options:any): Promise<{ 29 | instance: any; 30 | items: any[]; 31 | }>; 32 | findItem(query: any): Promise; 33 | findInstance(query: any, options: any): Promise; 34 | findInstances(query: any, option: 'summary' | 'full'|any): Promise; 35 | findItems(query: any): Promise; 36 | deleteInstances(query?: any): Promise; 37 | install(); 38 | archive(query); 39 | find(FindParams) : Promise; 40 | } 41 | 42 | interface IModelsDatastore { 43 | get(query): Promise; 44 | getList(query): Promise; 45 | getSource(name: any,owner): Promise; 46 | getSVG(name: any,owner): Promise; 47 | save(name: any, bpmn: any, svg?: any,owner?): Promise; 48 | 49 | load(name: any,owner): Promise; 50 | loadModel(name: any,owner): Promise; 51 | findEvents(query: any,owner): Promise; 52 | rebuild(model?: any): Promise 53 | 54 | install(); 55 | import(data); 56 | saveModel(model: IBpmnModelData): Promise; 57 | deleteModel(name: any,owner): Promise; 58 | renameModel(name: any,newName: any,owner): Promise; 59 | } 60 | 61 | 62 | class EventData implements IEventData { 63 | elementId; 64 | type; 65 | subType; 66 | name; 67 | processId; 68 | signalId; 69 | messageId; 70 | // timer info 71 | expression; 72 | expressionFormat; // cron/iso 73 | referenceDateTime; // start time of event or last time timer ran 74 | maxRepeat; 75 | repeatCount; 76 | timeDue?: Date; 77 | } 78 | 79 | 80 | export { IDataStore , IModelsDatastore } -------------------------------------------------------------------------------- /src/interfaces/elements.ts: -------------------------------------------------------------------------------- 1 | import { IItem, ILogger, NODE_ACTION, EXECUTION_EVENT, ITEM_STATUS, Node } from "../" 2 | 3 | interface IDefinition { 4 | name: any; 5 | processes: Map; 6 | rootElements: any; 7 | nodes: Map; 8 | flows: any[]; 9 | source: any; 10 | logger: any; 11 | accessRules: any[]; 12 | load(): Promise; 13 | getJson(): string; 14 | getDefinition(source: any, logger: ILogger): Promise; 15 | getStartNode(): Node; 16 | getNodeById(id: any): Node; 17 | } 18 | 19 | 20 | interface IElement { 21 | id: any; 22 | type: any; 23 | name: any; 24 | lane: any; 25 | behaviours: Map; 26 | continue(item: IItem): void; 27 | describe(): string[][]; 28 | restored(item: IItem): void; 29 | resume(item: IItem): void; 30 | /** 31 | * respond by providing behaviour attributes beyond item and node information 32 | * ex: timer due , input/outupt , fields 33 | * */ 34 | hasBehaviour(name: any): boolean; 35 | getBehaviour(name: any): any; 36 | addBehaviour(nane: any, behavriour: any): void; 37 | } 38 | interface IFlow extends IElement { 39 | 40 | } 41 | interface INode extends IElement { 42 | name: any; 43 | processId: any; 44 | def: any; 45 | outbounds: any[]; 46 | inbounds: any[]; 47 | doEvent(item: IItem, event: EXECUTION_EVENT, newStatus: ITEM_STATUS): Promise; 48 | enter(item: IItem): void; 49 | requiresWait(): boolean; 50 | canBeInvoked(): boolean; 51 | /** 52 | * this is the primary exectuion method for a node 53 | * 54 | * considerations: the following are handled by Token 55 | * 1. Loops we are inside a loop already (if any) 56 | * 2. Gatways 57 | * 3. Subprocess the parent node is fired as normal 58 | * run method will fire the subprocess invoking a new token and will go into wait 59 | */ 60 | execute(item: IItem): Promise; 61 | continue(item: IItem): Promise; 62 | start(item: IItem): Promise; 63 | run(item: IItem): Promise; 64 | end(item: IItem): Promise; 65 | /** 66 | * is called by the token after an execution resume for every active (in wait) item 67 | * different than init, which is called for all items 68 | * @param item 69 | */ 70 | resume(item: IItem): void; 71 | init(item: IItem): void; 72 | getOutbounds(item: IItem): IItem[]; 73 | 74 | } 75 | 76 | export {IDefinition , IElement , INode , IFlow } 77 | -------------------------------------------------------------------------------- /src/interfaces/engine.ts: -------------------------------------------------------------------------------- 1 | import { NODE_ACTION, TOKEN_STATUS } from './Enums'; 2 | import { IItemData , IInstanceData } from './'; 3 | import { ILogger, IAppDelegate, IBPMNServer, IDefinition, Token, Item, Element, Node, IServerComponent } from '../'; 4 | 5 | interface IScriptHandler { 6 | evaluateExpression(scope: IItem|IToken, expression); 7 | executeScript(scope: IItem|IExecution, script); 8 | 9 | } 10 | interface IToken { 11 | id: any; 12 | type; 13 | execution: IExecution; 14 | dataPath: string; 15 | startNodeId: any; 16 | parentToken?: IToken; 17 | // branchNode?: any; 18 | originItem: IItem; 19 | path: IItem[]; 20 | loop; 21 | currentNode: any; 22 | processId: any; 23 | status: TOKEN_STATUS; 24 | data; 25 | currentItem: IItem; 26 | lastItem: IItem; 27 | firstItem: Item; 28 | childrenTokens: Token[]; 29 | itemsKey: any; 30 | 31 | save(): { 32 | id: any; 33 | type; 34 | status: TOKEN_STATUS; 35 | dataPath: string; 36 | loopId: any; 37 | parentToken: any; 38 | originItem: any; 39 | startNodeId: any; 40 | currentNode: any; 41 | }; 42 | resume(): void; 43 | stop(): void; 44 | 45 | processError(errorCode,callingEvent); 46 | processEscalation(escalationCode,callingEvent); 47 | processCancel(callingEvent); 48 | 49 | 50 | restored(): void; 51 | getChildrenTokens(): any[]; 52 | preExecute(): Promise; 53 | preNext(): Promise; 54 | /** 55 | * this is the primary exectuion method for a token 56 | */ 57 | execute(inputData): Promise; 58 | appendData(inputData: any,item: IItem): void; 59 | /** 60 | * is called by Gateways to cancel current token 61 | * 62 | * */ 63 | terminate(): void; 64 | signal(data: any): Promise; 65 | getFullPath(fullPath?: any): Item[]; 66 | end(): Promise; 67 | goNext(): Promise; 68 | getSubProcessToken(): IToken; 69 | log(...msg: any): void; 70 | info(...msg: any): void; 71 | error(msg: any): void; 72 | } 73 | 74 | interface IExecution extends IServerComponent { 75 | instance: IInstanceData; 76 | server: IBPMNServer; 77 | tokens: Map; 78 | definition: IDefinition; 79 | appDelegate: IAppDelegate; 80 | logger: ILogger; 81 | process: any; 82 | promises; 83 | listener; 84 | isLocked:boolean; 85 | 86 | errors; 87 | item; 88 | messageMatchingKey; 89 | worker; 90 | userName; 91 | 92 | id; 93 | status; 94 | action: NODE_ACTION; 95 | options; 96 | name; 97 | 98 | getNodeById(id: any): Node; 99 | getToken(id: number): IToken; 100 | getItemsData(): IItemData[]; 101 | save(): Promise; 102 | end(): Promise; 103 | /** 104 | * 105 | * causes the execution to stop from running any further 106 | * */ 107 | stop(): void; 108 | terminate(): void; 109 | execute(startNodeId?: any, inputData?: {}): Promise; 110 | /** 111 | * 112 | * invoke scenarios: 113 | * itemId 114 | * elementId - but only one is active 115 | * elementId - for a startEvent in a secondary process 116 | * 117 | * @param executionId 118 | * @param inputData 119 | */ 120 | signalItem(executionId: any, inputData: any,options?:{}): Promise; 121 | signalEvent(executionId: any, inputData: any,options?:{}): Promise; 122 | signalRepeatTimerEvent(executionId,prevItem, inputData:any,options?:{}): Promise; 123 | 124 | getItems(query?: any): IItem[]; 125 | getState(): IInstanceData; 126 | restored(): void; 127 | resume(): void; 128 | report(): void; 129 | uids: {}; 130 | getNewId(scope: string): number; 131 | getUUID(): any; 132 | doExecutionEvent(process:any, event: any,eventDetails?:any): Promise; 133 | doItemEvent(item: any, event: any,eventDetails?: any): Promise; 134 | log(...msg: any): void; 135 | logS(...msg: any): void; 136 | logE(...msg: any): void; 137 | info(...msg: any): void; 138 | error(msg: any): void; 139 | appendData(inputData: any,item:IItem, dataPath?: any,assignment?:any): void; 140 | getData(dataPath: any): any; 141 | processQueue(): any; 142 | // getAndCreateData(dataPath: any, asArray?: boolean): any; 143 | } 144 | 145 | interface IItem extends IItemData { 146 | element: Element; 147 | token: Token; 148 | context: IExecution; 149 | node: Node; 150 | } 151 | 152 | 153 | export { IItem, IToken, IExecution , IScriptHandler} -------------------------------------------------------------------------------- /src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './Enums'; 3 | export * from './DataObjects'; 4 | export * from './common'; 5 | export * from './server'; 6 | export * from './engine'; 7 | export * from './datastore'; 8 | export * from './elements'; 9 | export * from './User'; -------------------------------------------------------------------------------- /src/interfaces/server.ts: -------------------------------------------------------------------------------- 1 | import { IExecution , ILogger , IItem, IConfiguration, IAppDelegate, IDataStore,IModelsDatastore, IScriptHandler } from '../'; 2 | import type { EventEmitter } from 'eventemitter3'; 3 | import type { EventEmitter as NodeEventEmitter } from 'events'; 4 | 5 | interface IBPMNServer { 6 | 7 | engine: IEngine; 8 | listener: EventEmitter | NodeEventEmitter; 9 | configuration: IConfiguration; 10 | logger: ILogger; 11 | definitions: IModelsDatastore; 12 | appDelegate: IAppDelegate; 13 | dataStore: IDataStore; 14 | cache: ICacheManager; 15 | cron: ICron; 16 | 17 | } 18 | 19 | interface IServerComponent { 20 | server: IBPMNServer; 21 | configuration: IConfiguration; 22 | logger: ILogger; 23 | cron: any; 24 | cache; 25 | appDelegate: IAppDelegate; 26 | engine: any; 27 | dataStore: IDataStore; 28 | scriptHandler: IScriptHandler; 29 | definitions; 30 | 31 | } 32 | 33 | interface IEngine { 34 | /** 35 | * loads a definitions and start execution 36 | * 37 | * @param name name of the process to start 38 | * @param data input data 39 | * @param startNodeId in process has multiple start node; you need to specify which one 40 | */ 41 | start(name: any, data?: any, startNodeId?: string, userName?: string, options?: any): Promise; 42 | /** 43 | * restores an instance into memeory or provides you access to a running instance 44 | * 45 | * this will also resume execution 46 | * 47 | * @param instanceQuery criteria to fetch the instance 48 | * 49 | * query example: 50 | * 51 | * ```jsonl 52 | * { id: instanceId} 53 | * { data: {caseId: 1005}} 54 | * { items.id : 'abcc111322'} 55 | * { items.itemKey : 'businesskey here'} 56 | * ``` 57 | * 58 | */ 59 | get(instanceQuery: any): Promise; 60 | /** 61 | * Continue an existing item that is in a wait state 62 | * 63 | * ------------------------------------------------- 64 | * 65 | * scenario: 66 | * 67 | * ``` 68 | * itemId {itemId: value } 69 | * itemKey {itemKey: value} 70 | * instance,task {instanceId: instanceId, elementId: value } 71 | * ``` 72 | * 73 | * @param itemQuery criteria to retrieve the item 74 | * @param data 75 | */ 76 | invoke(itemQuery: any, data: {}, userName?: string, options?: {}): Promise; 77 | 78 | assign(itemQuery: any, data: {}, assignment: {}, userName: string,options?:{}): Promise; 79 | 80 | 81 | startRepeatTimerEvent(instanceId, prevItem: IItem, data: {},options?:{}) : Promise; 82 | 83 | /** 84 | * 85 | * Invoking an event (usually start event of a secondary process) against an existing instance 86 | * or 87 | * Invoking a start event (of a secondary process) against an existing instance 88 | * ---------------------------------------------------------------------------- 89 | * instance,task 90 | *``` 91 | * {instanceId: instanceId, elementId: value } 92 | *``` 93 | 94 | * 95 | * @param instanceId 96 | * @param elementId 97 | * @param data 98 | */ 99 | startEvent(instanceId: any, elementId: any, data?: {},userName?: string,options?:{}): Promise; 100 | /** 101 | * 102 | * signal/message raise a signal or throw a message 103 | * 104 | * will seach for a matching event/task given the signalId/messageId 105 | * 106 | * that can be againt a running instance or it may start a new instance 107 | * ---------------------------------------------------------------------------- 108 | * @param messageId the id of the message or signal as per bpmn definition 109 | * @param matchingKey should match the itemKey (if specified) 110 | * @param data message data 111 | */ 112 | //signal(messageId: any, matchingKey: any, data?: {}): Promise; 113 | throwMessage(messageId, data: {}, matchingQuery: {}): Promise; 114 | throwSignal(signalId, data: {}, matchingQuery: {}); 115 | restart(itemQuery, data:any,userName, options) :Promise; 116 | 117 | upgrade(model:string,afterNodeIds:string[]):Promise; 118 | 119 | } 120 | 121 | interface ICron { 122 | 123 | checkTimers(duration); 124 | start(); 125 | startTimers(); 126 | 127 | } 128 | 129 | 130 | interface ICacheManager { 131 | list(); 132 | add(execution: IExecution); 133 | remove(instanceId); 134 | shutdown(); 135 | } 136 | 137 | 138 | export { IBPMNServer , IEngine , ICron ,ICacheManager , IServerComponent } -------------------------------------------------------------------------------- /src/scripts/convertTomd.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const readline = require('readline'); 3 | 4 | var myArgs = process.argv.slice(2); 5 | console.log('myArgs: ', myArgs); 6 | if (myArgs.length<2) { 7 | console.log("Syntax inputFile outputFile"); 8 | return; 9 | } 10 | const inputFile =myArgs[0]; 11 | const outputFile =myArgs[1]; 12 | console.log('generating '+inputFile +' into '+outputFile); 13 | 14 | 15 | const readInterface = readline.createInterface({ 16 | input: fs.createReadStream(inputFile), 17 | output: process.stdout, 18 | console: false 19 | }); 20 | 21 | console.log('-------------------------------------------------------'); 22 | 23 | const lines=[]; 24 | readInterface.on('line', function(line) { 25 | 26 | 27 | if (line.startsWith('///')) 28 | line=line.substring(3); 29 | 30 | console.log('=>'+line); 31 | lines.push(line); 32 | 33 | }); 34 | readInterface.on('close', function () { 35 | 36 | console.log("Writing" + lines.length); 37 | var id = fs.openSync(outputFile, 'w', 666); 38 | 39 | lines.forEach(line=>{ 40 | fs.writeSync(id, line + "\n", null, 'utf8'); 41 | 42 | }); 43 | 44 | fs.closeSync(id ); 45 | console.log("done"); 46 | console.log(' end -------------------------------------------------------'); 47 | 48 | }); 49 | 50 | -------------------------------------------------------------------------------- /src/scripts/copy-readme.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | 3 | const sourcePath = './README.md'; 4 | const destinationPath = './docs/README.md'; 5 | 6 | // Read the content of the source file 7 | fs.readFile(sourcePath, 'utf8', (err, data) => { 8 | if (err) { 9 | console.error('Error reading file:', err); 10 | return; 11 | } 12 | 13 | // Replace markdown links of the format '[text](./docs/xxx)' with '[text](xxx)' 14 | const updatedData = data.replace(/\]\(\.\/docs\/(.*?)\)/g, ']($1)'); 15 | 16 | // Write the updated content to the destination file 17 | fs.writeFile(destinationPath, updatedData, 'utf8', (err) => { 18 | if (err) { 19 | console.error('Error writing file:', err); 20 | return; 21 | } 22 | console.log(`File copied from ${sourcePath} to ${destinationPath} with internal links updated.`); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/scripts/example.ts: -------------------------------------------------------------------------------- 1 | // example.ts 2 | /** 3 | * Example type 4 | */ 5 | export type MyType = { 6 | foo: string; 7 | bar: number; 8 | }; 9 | /** 10 | * Example Interface ABC 11 | * just text here 12 | * and here too 13 | */ 14 | export interface abc { 15 | var1: string; 16 | } -------------------------------------------------------------------------------- /src/scripts/generate-toc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // From https://github.com/hapijs/joi/blob/master/generate-readme-toc.js 3 | 4 | // Load modules 5 | 6 | const Toc = require('markdown-toc'); 7 | const Fs = require('fs'); 8 | const {version} = require('../../package.json'); 9 | 10 | // Declare internals 11 | 12 | const filenames = getFileNames(); 13 | console.log('Current directory: ' + process.cwd()); 14 | console.log(filenames); 15 | 16 | 17 | function getFileNames() { 18 | 19 | const arg = process.argv[2] || './API.md'; 20 | 21 | return arg.split(','); 22 | } 23 | 24 | function generate(filename) { 25 | const api = Fs.readFileSync(filename, 'utf8'); 26 | const tocOptions = { 27 | bullets: '-', 28 | maxdepth: 3, 29 | slugify: function(text) { 30 | 31 | return text.toLowerCase() 32 | .replace(/\s/g, '-') 33 | .replace(/[^\w-]/g, ''); 34 | } 35 | }; 36 | 37 | const output = Toc.insert(api, tocOptions) 38 | .replace(/(.|\n)*/, '\n# ' + version + ' API Reference\n'); 39 | 40 | Fs.writeFileSync(filename, output); 41 | } 42 | 43 | filenames.forEach(generate); -------------------------------------------------------------------------------- /src/scripts/tsToApi.ts: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | 4 | run: 5 | 6 | npx typedoc --json api.json 7 | 8 | * sample output 9 | @Get('/model') 10 | async getModelList() { 11 | var list=await this.api.model.list({},SystemUser); 12 | return list; 13 | } 14 | //get all workflow 15 | @Get('/') 16 | getWorkflows() { 17 | return this.api.data.getPendingUserTasks({},SystemUser); 18 | } 19 | //create workflow 20 | @Post('/:workflowname') 21 | @ApiBody({ description: 'create worflow, submit data in json format', type:SampleApiSchema}) 22 | @ApiOperation({ 23 | operationId: 'startWorkflow', 24 | description: 'start new workflow', 25 | }) 26 | startWorkflow( 27 | @Param('workflowname') workflowname:string, 28 | @Body() data: Object, 29 | ) { 30 | return this.bpmnService.startWorkflow(workflowname,data); 31 | } 32 | 33 | -- Additional options: 34 | @Get(path) 35 | @Body() 36 | 37 | */ 38 | 39 | import * as path from "path"; 40 | import * as ts from "typescript"; 41 | import * as fs from 'fs'; 42 | const input={}; 43 | function main(input) { 44 | 45 | } 46 | class sModule { 47 | name; 48 | comment; 49 | members=[]; 50 | constructor(json) { 51 | this.name=json.name; 52 | this.comment=json.comment.summary[0].text; 53 | json.children.forEach(child=>{ 54 | 55 | this.members.push(new sMember(child)); 56 | }); 57 | 58 | 59 | } 60 | Out() { 61 | let txt=`//---------------------------- ${this.name} 62 | //---------------------------- 63 | ${this.comment} 64 | 65 | //---------------------------- 66 | 67 | ` 68 | this.members.forEach(m=>{ txt+=''+m.out();}); 69 | txt+=``; 70 | return txt; 71 | } 72 | } 73 | class sMember { 74 | name; 75 | comment; 76 | isAsync; 77 | returnType; 78 | params=[]; 79 | constructor(m) { 80 | this.name=m.name; 81 | 82 | m.signatures.forEach(sign=>{ 83 | sign.comment.summary.forEach(l=>{ this.comment=l.text;}); 84 | 85 | this.returnType= sign.type.name; 86 | if (sign.type.typeArguments) 87 | sign.type.typeArguments.forEach(tt=>{this.returnType=tt.name;}); 88 | 89 | sign.parameters.forEach(param=>{ 90 | this.params.push(new sParam(param)); 91 | }); 92 | 93 | }); 94 | 95 | } 96 | /** 97 | * 98 | * @returns 99 | */ 100 | out() { 101 | let txt=` 102 | //---------------------------- 103 | // ${this.name} 104 | //${this.comment} 105 | 106 | ${this.name}:${this.returnType}(`; 107 | this.params.forEach(p=>{txt+=` `+p.out();}) 108 | txt+=`) { 109 | } 110 | `; 111 | return txt; 112 | } 113 | } 114 | class sParam{ 115 | name; 116 | type='any'; 117 | isOptional=false; 118 | comment=''; 119 | constructor (param) 120 | { 121 | this.name=param.name; 122 | if (param.comment && param.comment.summary) 123 | this.comment=' -"'+param.comment.summary[0].text+'"'; 124 | 125 | if (param.flags && param.flags.isOptional) 126 | this.isOptional=true; 127 | if (param.type.type=='reference') 128 | this.type=param.type.name; 129 | 130 | } 131 | out() { 132 | let txt=`${this.name}:${this.type} ${this.comment}, `; 133 | return txt; 134 | 135 | } 136 | 137 | } 138 | 139 | getClass("IAPIEngine"); 140 | 141 | function getClass(name) { 142 | 143 | const json=fs.readFileSync('api.json'); 144 | const src=JSON.parse(json+''); 145 | 146 | src.children.forEach(cls=>{ 147 | if (cls.name==name) 148 | { 149 | let sCls = new sModule(cls); 150 | let txt=''; 151 | txt=sCls.Out(); 152 | console.log('txt:',txt); 153 | 154 | } 155 | 156 | } ); 157 | } 158 | -------------------------------------------------------------------------------- /src/server/BPMNServer.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '../common/Logger'; 2 | import { IConfiguration, ILogger, IAppDelegate, IBPMNServer, IDataStore, ICacheManager, IScriptHandler } from '../'; 3 | import { Engine } from './Engine'; 4 | import { Cron } from './Cron'; 5 | import { EventEmitter } from 'eventemitter3'; 6 | 7 | /** 8 | * The main class of Server Layer 9 | * provides the full functionalities: 10 | * 11 | * at start of the app: 12 | * new BPMNServer(configuration,options); 13 | * 14 | * after that point: 15 | * 16 | * BPMNServer.engine.start(...) 17 | * BPMNServer.engine.invoke(...) 18 | * BPMNServer.dataStore.findInstances(...) 19 | * BPMNServer.dataStore.findItems(...) 20 | */ 21 | 22 | /* removing the following block causes :#222 23 | restored in release 2.2.11 24 | */ 25 | 26 | process.on('uncaughtException', function (err) { 27 | console.log('***************BPMNServer UNCAUGHT ERROR***********'); 28 | try { 29 | BPMNServer.getInstance().error = err; 30 | BPMNServer.getInstance().logger.reportError(err); 31 | } 32 | catch (exc) { 33 | console.log(err); 34 | } 35 | return; 36 | }); 37 | 38 | class BPMNServer implements IBPMNServer { 39 | 40 | engine: Engine; 41 | listener: EventEmitter; 42 | configuration: IConfiguration; 43 | logger: ILogger; 44 | definitions; 45 | appDelegate: IAppDelegate; 46 | dataStore: IDataStore; 47 | cache: ICacheManager; 48 | scriptHandler: IScriptHandler; 49 | cron: Cron; 50 | error; 51 | 52 | private static instance: BPMNServer; 53 | 54 | /** 55 | * Server Constructor 56 | * 57 | * @param configuration see 58 | * @param logger 59 | */ 60 | 61 | constructor(configuration: IConfiguration, logger?: ILogger, options = {}) { 62 | 63 | if (logger == null) { 64 | logger = new Logger({}); 65 | } 66 | this.listener = new EventEmitter(); 67 | this.logger = logger; 68 | this.configuration = configuration; 69 | this.cron = new Cron(this); 70 | this.engine = new Engine(this); 71 | 72 | this.cache = configuration.cacheManager(this); 73 | this.dataStore = configuration.dataStore(this); 74 | this.definitions = configuration.definitions(this); 75 | this.appDelegate = configuration.appDelegate(this); 76 | this.scriptHandler = configuration.scriptHandler(this); 77 | 78 | //this.acl = new ACL(this); 79 | //this.iam = new IAM(this); 80 | console.log("bpmn-server version " + BPMNServer.getVersion()); 81 | BPMNServer.instance=this; 82 | 83 | this.appDelegate.startUp(options); 84 | 85 | if (options['cron'] == false) { 86 | return; 87 | } 88 | else { 89 | this.cron.start(); 90 | } 91 | } 92 | status() { 93 | return { 94 | version: BPMNServer.getVersion(), 95 | cache: this.cache.list, 96 | engineRunning: this.engine.runningCounter, 97 | engineCalls: this.engine.callsCounter, 98 | memoryUsage: typeof __dirname === 'undefined' ? require('node:process').memoryUsage() : null, 99 | }; 100 | } 101 | static getVersion() { 102 | if (typeof __dirname === 'undefined') return 'unknown'; 103 | const configPath = __dirname+'/../../package.json'; 104 | const fs = require('fs'); 105 | 106 | if (fs.existsSync(configPath)) { 107 | 108 | var configuration = JSON.parse(fs.readFileSync(configPath, 'utf8')); 109 | var _version = configuration['version']; 110 | return _version; 111 | } 112 | else 113 | return 'cannot locate package.json current: ' + __dirname+' path '+configPath; 114 | } 115 | public static get engine() { 116 | return BPMNServer.getInstance().engine; 117 | } 118 | public static getInstance(): BPMNServer { 119 | if (!BPMNServer.instance) { 120 | throw Error("BPMN Server Not initialized"); 121 | } 122 | 123 | return BPMNServer.instance; 124 | } 125 | } 126 | 127 | 128 | export { BPMNServer}; 129 | -------------------------------------------------------------------------------- /src/server/CacheManager.ts: -------------------------------------------------------------------------------- 1 | import { ServerComponent} from './ServerComponent'; 2 | import { EXECUTION_EVENT, ICacheManager, IExecution } from '../interfaces'; 3 | 4 | class NoCacheManager extends ServerComponent implements ICacheManager { 5 | 6 | constructor(server) { 7 | super(server); 8 | } 9 | 10 | list() {return []; } 11 | /** 12 | **/ 13 | getInstance(instanceId) { return null; } 14 | 15 | add(execution:IExecution) { return null; } 16 | remove(instanceId) {return null; } 17 | // shutsdown all instances that are still live 18 | shutdown() { } 19 | restart() { } 20 | } 21 | 22 | class CacheManager extends ServerComponent implements ICacheManager { 23 | static liveInstances = new Map(); 24 | 25 | constructor(server) { 26 | super(server); 27 | 28 | var self = this; 29 | server.listener.on(EXECUTION_EVENT.process_end, 30 | function ({ context, event, }) { 31 | // console.log(`--->Cache Event: ${event} Removing Instance:`, context.instance.id); 32 | self.remove(context.instance.id); 33 | }); 34 | 35 | } 36 | 37 | list() { 38 | const items = []; 39 | CacheManager.liveInstances.forEach(item => { items.push(item); }); 40 | return items; 41 | 42 | } 43 | /** 44 | **/ 45 | getInstance(instanceId) { 46 | 47 | const instance = CacheManager.liveInstances.get(instanceId); 48 | return instance; 49 | } 50 | 51 | add(execution:IExecution) { 52 | 53 | CacheManager.liveInstances.set(execution.id, execution); 54 | } 55 | remove(instanceId) { 56 | CacheManager.liveInstances.delete(instanceId); 57 | } 58 | // shutsdown all instances that are still live 59 | async shutdown() { 60 | 61 | this.logger.log("Shutdown.."); 62 | const instances = CacheManager.liveInstances; 63 | let i = 0; 64 | const list = []; 65 | instances.forEach(item => { list.push(item); }); 66 | for (i = 0; i < list.length; i++) { 67 | 68 | const engine = list[i]; 69 | //await engine.stop(); 70 | this.logger.log("shutdown engine " + engine.execution.name + " status : " + engine.execution.state); 71 | instances.delete(list[i].execution.id); 72 | } 73 | } 74 | } 75 | export { CacheManager , NoCacheManager}; 76 | -------------------------------------------------------------------------------- /src/server/Cron.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Logger } from '../common/Logger'; 3 | 4 | import { ServerComponent } from './ServerComponent'; 5 | 6 | import { IEventData, ICron } from '../'; 7 | 8 | const duration = require('iso8601-duration'); 9 | const parse = duration.parse; 10 | const end = duration.end; 11 | const toSeconds = duration.toSeconds; 12 | 13 | class Cron extends ServerComponent implements ICron { 14 | 15 | private static timersStarted = false; 16 | private static checkingTimers = false; 17 | private static timersFired = 0; 18 | 19 | static timerScheduled(timeDue) { 20 | Cron.timersFired++; 21 | } 22 | static timerEnded(item) { 23 | Cron.timersFired--; 24 | 25 | } 26 | async checkTimers(duration = 0) { 27 | return; 28 | } 29 | async start() { 30 | this.startTimers(); 31 | } 32 | async startTimers() { 33 | 34 | if (Cron.timersStarted) 35 | return; 36 | Cron.timersStarted = true; 37 | 38 | try { 39 | await this.definitions.rebuild(); 40 | } 41 | catch(exc) 42 | { 43 | console.log(exc); 44 | } 45 | 46 | // this.logger.log("Start timers"); 47 | 48 | let promises = []; 49 | const self = this; 50 | try { 51 | 52 | let precision = this.configuration.timers.precision; 53 | if (!precision) 54 | precision = 3000; 55 | let target = new Date().getTime(); 56 | let query, list, i; 57 | target += precision; 58 | 59 | query = { "events.subType": "Timer" }; 60 | list = await self.definitions.findEvents(query); 61 | 62 | for (i = 0; i < list.length; i++) { 63 | let entry = list[i]; 64 | if (entry.timeDue) { 65 | this.scheduleProcess(entry) 66 | } 67 | } 68 | 69 | // { "items.timeDue": { $lt: new ISODate("2020-06-14T19:44:38.541Z") } } 70 | // query = { "items.timeDue": { $lt: target } }; 71 | query = { "items.timeDue": { $exists: true }, "items.status": "wait" }; 72 | 73 | //query = { query: { "items.timeDue": { $lt: new Date(target).toISOString() } } }; 74 | //query = { items: {timeDue: { $lt: new Date(target).toISOString() } }}; 75 | list = await this.dataStore.findItems(query); 76 | // this.logger.log("...items query returend " + list.length); 77 | for (i = 0; i < list.length; i++) { 78 | let item = list[i]; 79 | if (item.timeDue) { 80 | let now = new Date(); 81 | self.logger.log(`...checking timer: ${item.timeDue}`); 82 | 83 | this.scheduleItem(item); 84 | } 85 | } 86 | } 87 | catch (exc) { 88 | console.log(exc); 89 | } 90 | await Promise.all(promises); 91 | 92 | //this.logger.log("... all timers are done."); 93 | } 94 | 95 | private async itemTimerExpired() { 96 | const entry: any = this as any; 97 | await entry.cron.engine.invoke({ "items.id": entry.id }, null); 98 | } 99 | private async processTimerExpired() { 100 | const params: any = this as any; 101 | const event = params.entry; 102 | const cron = params.cron; 103 | 104 | await cron.definitions.updateTimer(event.modelName); 105 | 106 | event.referenceDateTime = new Date().getTime(); 107 | cron.scheduleProcess(event); 108 | 109 | await cron.engine.start(event.modelName, null, null, event.elementId); 110 | } 111 | private scheduleProcess( entry) { 112 | const delay = Cron.timeDue(entry.expression,entry.referenceDateTime); 113 | if (delay) { 114 | 115 | const scheduleAt = new Date(delay * 1000 + new Date().getTime()); 116 | console.log("scheduling process " + entry.modelName + " delayed by " + delay + " seconds, scheduled at: " + scheduleAt ); 117 | if (delay < 0) 118 | setTimeout(this.processTimerExpired.bind({ entry, cron: this }), 100); 119 | else 120 | setTimeout(this.processTimerExpired.bind({ entry, cron: this }), delay * 1000); 121 | 122 | } 123 | } 124 | private scheduleItem(entry) { 125 | 126 | const now = new Date().getTime(); 127 | let delay; 128 | delay = entry.timeDue - now; 129 | if (delay < 0) delay = .1; 130 | entry.cron = this; 131 | setTimeout(this.itemTimerExpired.bind(entry), delay ); 132 | } 133 | 134 | static checkCron(expression,referenceDateTime) { 135 | 136 | var parser = require('cron-parser'); 137 | const now = new Date().getTime(); 138 | 139 | try { 140 | var options = { 141 | currentDate: referenceDateTime 142 | }; 143 | 144 | const interval = parser.parseExpression(expression, options); 145 | const next = interval.next(); 146 | const delay = (next.getTime()- now)/1000; 147 | return delay; 148 | } catch (err) { 149 | return null; 150 | } 151 | 152 | } 153 | static timeDue(expression,referenceDateTime) { 154 | if (expression) { 155 | let baseDate = new Date(); 156 | let delay; 157 | const now = new Date().getTime(); 158 | if (referenceDateTime) { 159 | baseDate = new Date(referenceDateTime); 160 | } 161 | try { 162 | delay = Cron.checkCron(expression, baseDate); 163 | if (delay) { 164 | console.log(" expression " + expression + " base date" + baseDate+ " -> delay of " + delay + " sec " + delay / 60 + " min" + delay/3600 + " hours "); 165 | } 166 | else { 167 | delay = toSeconds(parse(expression)); 168 | if (referenceDateTime) { 169 | delay += (referenceDateTime - now) / 1000; 170 | } 171 | // console.log(" expression " + expression + " base date" + baseDate + " -> delay of " + delay + " sec " + delay / 60 + " min" + delay / 3600 + " hours "); 172 | } 173 | } 174 | catch (exc) { 175 | console.log(exc,'expression',expression); 176 | return null; 177 | } 178 | 179 | return delay; 180 | } 181 | return null; 182 | } 183 | /** 184 | * Schedule Scripts 185 | * script,itemId(scope),dateTime,cancelCondition 186 | * 1. ScheduleScripts is called by app 187 | * 2. Save in DB in case of system failure or shutdown 188 | * 3. if due within period issue setTime 189 | * 4. checkForDueScripts: Check periodically for prev. old scripts and scripts due in next prev. 190 | * Prev. scripts fire now 191 | * executeScript 192 | * due issue setTimeout for them 193 | * setTimerForScript 194 | * 195 | * 196 | **/ 197 | } 198 | 199 | class ScriptScheduler { 200 | script; 201 | itemId; 202 | dateDue; 203 | cancelCondition; 204 | constructor(script, item, dateDue, cancelCondition) { 205 | this.script = script; 206 | this.itemId = item.id; 207 | if (dateDue) {// within period 208 | this.executeScript(item); 209 | } 210 | } 211 | executeScript(item = null) { 212 | if (item) { // no need to retrieve just fire it 213 | 214 | } 215 | } 216 | save() { 217 | 218 | } 219 | 220 | static checkForDueScripts() { 221 | 222 | } 223 | 224 | } 225 | export { Cron}; 226 | -------------------------------------------------------------------------------- /src/server/Listener.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "events"; 2 | import { IExecution } from "../interfaces"; 3 | 4 | class Listener extends EventEmitter { 5 | 6 | async doEvent(event,execution,item=null ,eventDetails={}) { 7 | execution.item = item; 8 | await this.delegateEvent(event, execution,eventDetails); 9 | await this.emit(event, { event, context: execution,eventDetails }); 10 | await this.emit('all', { event, context: execution,eventDetails }); 11 | } 12 | async delegateEvent(event, execution: IExecution,eventDetails) { 13 | 14 | const app= execution.appDelegate; 15 | const defDelegate = null;//execution.defDelegate; 16 | 17 | if (app[event]) { 18 | // method exists in the component 19 | await app[event](event,execution,eventDetails); // call it 20 | } 21 | if (defDelegate && defDelegate[event]) { 22 | // method exists in the component 23 | await defDelegate[event](event, execution,eventDetails); // call it 24 | } 25 | 26 | 27 | } 28 | 29 | 30 | } 31 | 32 | export {Listener} -------------------------------------------------------------------------------- /src/server/ServerComponent.ts: -------------------------------------------------------------------------------- 1 | import { Cron, CacheManager} from "./"; 2 | import { IEngine, IBPMNServer } from "../interfaces"; 3 | 4 | 5 | /** 6 | * super class for various objects that are part of the server 7 | * */ 8 | class ServerComponent { 9 | server; 10 | constructor(server: IBPMNServer) { 11 | this.server = server; 12 | } 13 | 14 | get configuration() { return this.server.configuration; } 15 | get logger() { return this.server.logger; } 16 | get cron(): Cron { return this.server.cron; }; 17 | get cache(): CacheManager { return this.server.cache; }; 18 | get appDelegate() { return this.server.appDelegate; } 19 | get engine(): IEngine { return this.server.engine; }; 20 | get dataStore() { return this.server.dataStore; } 21 | get definitions() { return this.server.definitions; } 22 | get listener() { return this.server.listener; } 23 | get scriptHandler() { return this.server.scriptHandler; } 24 | } 25 | 26 | export { ServerComponent } -------------------------------------------------------------------------------- /src/server/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BPMNServer'; 2 | export * from './Cron'; 3 | export * from './CacheManager'; 4 | export * from './ServerComponent'; 5 | export * from './Engine'; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "lib": [ "es6" , "es2021" ], 6 | "sourceMap": true, 7 | "skipLibCheck": true, 8 | "declaration": true, 9 | "rootDir": "./src", 10 | "outDir": "./dist" 11 | }, 12 | "include": [ 13 | "**/*.ts", "test/handler.js" 14 | ], 15 | "exclude": [ 16 | "node_modules", 17 | "./dist", 18 | "docusaurus.config.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ "tslint:recommended", "tslint-config-prettier" ] 3 | , 4 | "exclude": [ 5 | "**/bin", 6 | "**/bower_components", 7 | "**/jspm_packages", 8 | "**/node_modules", 9 | "**/obj", 10 | "**/platforms" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://typedoc.org/schema.json", 3 | "entryPoints": ["./src/"], 4 | "out": "../docs/docs/api", 5 | "sort":"source-order", 6 | "compilerOptions": { 7 | "strictNullChecks": false, 8 | "skipLibCheck": true, 9 | "rootDir": ".", 10 | }, 11 | "readme": "none", 12 | "entryDocument": "readme.md", 13 | "plugin": ["typedoc-plugin-markdown"] 14 | } --------------------------------------------------------------------------------