├── .dockerignore ├── .eslintrc.js ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .prettierrc.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── HOW_TO_USE.md ├── LICENSE.md ├── README.md ├── SECURITY.md ├── docker-compose.yml ├── e2e └── config.json ├── envs ├── .env.development └── .env.local ├── examples ├── Employee Hiring │ ├── hiring.json │ └── hiring.png ├── Invoice Processing │ ├── README.md │ ├── invoice_processing.json │ └── invoice_processing_image.PNG └── Travel Reimbursement │ ├── README.md │ ├── travel_reimbursement.json │ └── travel_reimbursement_image.png ├── nano.save ├── nest-cli.json ├── package-lock.json ├── package.json ├── sonar-project.properties ├── src ├── app.module.ts ├── common │ ├── const │ │ ├── BPMN-mappings.ts │ │ ├── constants.ts │ │ ├── custom-messages.ts │ │ └── enums.ts │ ├── exceptions │ │ ├── custom-error.ts │ │ ├── http-exception.filter.ts │ │ └── index.ts │ ├── guards │ │ └── rbac.interface.ts │ ├── interceptors │ │ ├── index.ts │ │ └── response.interceptor.ts │ ├── middlewares │ │ ├── http-request-headers.middleware.ts │ │ ├── index.ts │ │ └── logger.middleware.ts │ ├── providers │ │ ├── app-cluster.service.ts │ │ ├── custom-response.service.ts │ │ ├── execute-only-once.ts │ │ └── node-grpc-client.ts │ └── utils │ │ └── date-utils.ts ├── main.ts ├── models │ ├── process-definitions │ │ ├── process-definitions.schema.ts │ │ └── repository │ │ │ ├── process-definitions.repository.d.ts │ │ │ └── process-definitions.repository.impl.ts │ ├── process-instances │ │ ├── process-instances.schema.ts │ │ └── repository │ │ │ ├── process-instances.repository.d.ts │ │ │ └── process-instances.repository.impl.ts │ └── user-tasks │ │ ├── repository │ │ ├── user-tasks.repository.d.ts │ │ └── user-tasks.repository.impl.ts │ │ └── user-tasks.schema.ts ├── modules │ ├── index.ts │ ├── process-definitions │ │ ├── dtos │ │ │ ├── create-process-definition.dto.ts │ │ │ ├── get-process-definitions.dto.ts │ │ │ ├── index.ts │ │ │ ├── update-process-definition.dto.ts │ │ │ └── update-stage.dto.ts │ │ ├── joi-validations │ │ │ └── create-process-definition.joi.ts │ │ ├── process-definitions.grpc.controller.ts │ │ ├── process-definitions.module.ts │ │ ├── process-definitions.rest.controller.ts │ │ ├── process-definitions.service.ts │ │ └── providers │ │ │ └── multer-options.ts │ ├── process-instances │ │ ├── dtos │ │ │ ├── create-process-instance.rest.dto.ts │ │ │ ├── get-process-instances.rest.dto.ts │ │ │ ├── index.ts │ │ │ ├── process-instance.dto.ts │ │ │ ├── start-process-instance.rest.dto.ts │ │ │ ├── stats.rest.dto.ts │ │ │ └── update-process-instance.dto.ts │ │ ├── process-instances.module.ts │ │ ├── process-instances.rest.controller.ts │ │ ├── process-instances.service.ts │ │ └── providers │ │ │ ├── compiler.ts │ │ │ └── index.ts │ └── tasks │ │ ├── dtos │ │ ├── complete-task.rest.dto.ts │ │ ├── create-user-task.dto.ts │ │ ├── get-mytasks.rest.dto.ts │ │ ├── get-tasks.rest.dto.ts │ │ ├── index.ts │ │ └── update-user-task.dto.ts │ │ ├── providers │ │ ├── executor.ts │ │ └── index.ts │ │ ├── tasks.module.ts │ │ ├── tasks.rest.controller.ts │ │ └── tasks.service.ts └── shared │ ├── clients │ └── common-requests.proto │ ├── connectors │ ├── grpc.connector.ts │ ├── http.connector.ts │ ├── index.ts │ └── openAI.connector.ts │ ├── dtos │ ├── common-headers.dto.ts │ ├── connectors.dto.ts │ └── index.ts │ └── repositories │ └── base │ ├── base-repository.impl.ts │ ├── base.entity.ts │ ├── base.error.ts │ └── base.repository.d.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json └── webpack.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | .vscode 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | src/shared/clients/gRPC/* 37 | workflows 38 | test 39 | e2e 40 | src/shared/public -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es2021: true, 4 | node: true, 5 | }, 6 | parser: '@typescript-eslint/parser', 7 | parserOptions: { 8 | project: './tsconfig.json', 9 | sourceType: 'module', 10 | tsconfigRootDir: __dirname, 11 | ecmaVersion: 12, 12 | }, 13 | extends: ['airbnb-base', 'airbnb-typescript/base', 'prettier'], 14 | root: true, 15 | ignorePatterns: ['.eslintrc.js'], 16 | rules: { 17 | 'no-console': 'off', 18 | 'max-classes-per-file': 'off', 19 | 'no-plusplus': 'off', 20 | 'import/prefer-default-export': 'off', 21 | "class-methods-use-this": "off", 22 | "no-useless-constructor": 0 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # These are some examples of commonly ignored file patterns. 2 | # You should customize this list as applicable to your project. 3 | # Learn more about .gitignore: 4 | # https://www.atlassian.com/git/tutorials/saving-changes/gitignore 5 | 6 | # Node artifact files 7 | .env 8 | node_modules/ 9 | dist 10 | # Compiled Java class files 11 | *.class 12 | 13 | # Compiled Python bytecode 14 | *.py[cod] 15 | 16 | # Log files 17 | *.log 18 | 19 | # Package files 20 | *.jar 21 | 22 | # Maven 23 | target/ 24 | dist/ 25 | 26 | # JetBrains IDE 27 | .idea/ 28 | 29 | # Unit test reports 30 | TEST*.xml 31 | 32 | # Generated by MacOS 33 | .DS_Store 34 | 35 | # Generated by Windows 36 | Thumbs.db 37 | 38 | # Applications 39 | *.app 40 | *.exe 41 | *.war 42 | 43 | # Large media files 44 | *.mp4 45 | *.tiff 46 | *.avi 47 | *.flv 48 | *.mov 49 | *.wmv 50 | src/shared/clients/gRPC/* 51 | src/shared/public -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | We are committed to providing a friendly, safe, and welcoming environment for all contributors and participants in the IO Flow project. We expect everyone involved to adhere to the following code of conduct. 4 | 5 | ## Our Pledge 6 | 7 | In the interest of fostering an open and inclusive community, we pledge to: 8 | 9 | - Be respectful and considerate towards others, regardless of their background, experiences, or opinions. 10 | - Welcome and support newcomers, making sure they feel valued and included. 11 | - Be patient and understanding when disagreements and conflicts arise. 12 | - Strive to create an inclusive and diverse community, where everyone feels comfortable and empowered to contribute. 13 | 14 | ## Expected Behavior 15 | 16 | To create a positive and inclusive environment, we expect all participants to: 17 | 18 | - Be kind, respectful, and empathetic towards others. 19 | - Use inclusive language and be mindful of our words and actions. 20 | - Accept constructive feedback gracefully and learn from it. 21 | - Be open-minded and consider different perspectives. 22 | - Focus on collaboration and finding solutions, rather than engaging in personal attacks or derogatory remarks. 23 | - Respect the privacy and personal boundaries of others. 24 | 25 | ## Unacceptable Behavior 26 | 27 | The following behaviors are considered unacceptable and will not be tolerated: 28 | 29 | - Harassment, discrimination, or any form of offensive or discriminatory behavior. 30 | - Personal attacks, insults, or derogatory comments towards others. 31 | - Intimidation, threats, or any form of bullying. 32 | - Any behavior that causes discomfort or distress to others. 33 | - Sharing or distributing others' private or personal information without their consent. 34 | - Any other behavior that goes against the principles of a respectful and inclusive community. 35 | 36 | ## Reporting Incidents 37 | 38 | If you witness or experience any unacceptable behavior while participating in the IO Flow project, please report it by contacting the project maintainers at [email address or other appropriate contact information]. All reports will be handled confidentially and with respect for the privacy of individuals involved. We are committed to promptly addressing and resolving any reported issues. 39 | 40 | ## Consequences of Unacceptable Behavior 41 | 42 | Unacceptable behavior will not be tolerated and may result in consequences, including but not limited to: 43 | 44 | - A warning or request for clarification or apology. 45 | - Temporary or permanent exclusion from the project. 46 | - Blocking or banning from project communication channels and events. 47 | 48 | ## Attribution 49 | 50 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html) version 2.0. We thank the contributors of the Contributor Covenant for their valuable work in creating a foundation for promoting positive interactions in open source communities. 51 | 52 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Welcome to the IO Flow project! We appreciate your interest in contributing. By participating in this project, you agree to abide by the [Code of Conduct](CODE_OF_CONDUCT.md). This document outlines the guidelines and process for contributing to our project. Please take a moment to review the guidelines before getting started. 4 | 5 | ## Table of Contents 6 | - [Reporting Issues](#reporting-issues) 7 | - [Making Contributions](#making-contributions) 8 | - [Coding Standards](#coding-standards) 9 | - [Commit Guidelines](#commit-guidelines) 10 | - [Submitting Pull Requests](#submitting-pull-requests) 11 | - [Code of Conduct](CODE_OF_CONDUCT.md) 12 | 13 | ## Reporting Issues 14 | 15 | If you encounter any issues while using or developing IO Flow, please report them using the issue tracker. Before submitting an issue, please check if a similar issue already exists. Include as much detail as possible, such as the version of IO Flow, steps to reproduce the issue, and relevant logs or error messages. 16 | 17 | ## Making Contributions 18 | 19 | We welcome contributions from everyone. To contribute to IO Flow, follow these steps: 20 | 21 | 1. Fork the repository to your GitHub account. 22 | 2. Create a new branch for your feature or bug fix: `git checkout -b your-branch-name`. 23 | 3. Make your changes and ensure they adhere to our [coding standards](#coding-standards). 24 | 4. Write tests to cover your changes, if applicable. 25 | 5. Commit your changes with a clear and descriptive commit message using our [commit guidelines](#commit-guidelines). 26 | 6. Push your branch to your forked repository. 27 | 7. Open a pull request to the main repository's `master` branch, providing a detailed description of your changes. 28 | 8. Participate in the discussion and address any feedback or suggestions received during the code review process. 29 | 9. Once your pull request is approved, it will be merged into the main repository. 30 | 31 | ## Coding Standards 32 | 33 | We strive to maintain a high-quality codebase, and we encourage contributors to follow these coding standards: 34 | The codebase is developed mainly by using the [airbnb](https://airbnb.io/javascript/) standards. 35 | 36 | - Use clear and descriptive variable and function names. 37 | - Follow the project's existing code style and formatting conventions. 38 | - Include inline comments to explain complex logic or important details. 39 | - Write concise and meaningful documentation where necessary. 40 | - Ensure your code passes all relevant tests and does not introduce new linting errors. 41 | 42 | ## Commit Guidelines 43 | 44 | To keep the commit history clean and organized, we follow the [Conventional Commits](https://www.conventionalcommits.org/) specification. Please structure your commit messages as follows: 45 | 46 | : 47 | 48 | [optional body] 49 | 50 | [optional footer] 51 | 52 | markdown 53 | Copy code 54 | 55 | The `` can be one of the following: 56 | - `feat`: A new feature or enhancement 57 | - `fix`: A bug fix 58 | - `docs`: Documentation changes 59 | - `refactor`: Code refactoring 60 | - `test`: Adding or modifying tests 61 | - `chore`: Routine tasks, maintenance, or other non-code changes 62 | 63 | The `` should be a clear and concise summary of the change. 64 | 65 | The `[optional body]` section should provide additional context or details about the change if necessary. 66 | 67 | The `[optional footer]` section should reference any issues or pull requests related to the change. 68 | 69 | ## Submitting Pull Requests 70 | 71 | When submitting a pull request, please provide the following information in the description: 72 | 73 | - A summary of the changes made and the problem they solve. 74 | - Any relevant information that might help with the review process. 75 | - If applicable, reference any related issues or pull requests. 76 | 77 | ## Code of Conduct 78 | 79 | Please review and adhere to our [Code of Conduct](CODE_OF_CONDUCT.md) throughout 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ################### 2 | # BUILD FOR LOCAL DEVELOPMENT 3 | ################### 4 | 5 | FROM node:18-alpine As development 6 | 7 | WORKDIR /usr/src/app 8 | 9 | COPY --chown=node:node package*.json ./ 10 | 11 | RUN npm ci 12 | 13 | COPY --chown=node:node . . 14 | 15 | USER node 16 | 17 | ################### 18 | # BUILD FOR PRODUCTION 19 | ################### 20 | 21 | FROM node:18-alpine As build 22 | 23 | WORKDIR /usr/src/app 24 | 25 | COPY --chown=node:node package*.json ./ 26 | 27 | COPY --chown=node:node --from=development /usr/src/app/node_modules ./node_modules 28 | 29 | COPY --chown=node:node . . 30 | 31 | RUN npm run build 32 | 33 | ARG NODE_ENV=production 34 | ENV NODE_ENV=${NODE_ENV} 35 | 36 | RUN npm ci --only=production && npm cache clean --force 37 | 38 | USER node 39 | 40 | ################### 41 | # PRODUCTION 42 | ################### 43 | 44 | FROM node:18-alpine As production 45 | 46 | COPY --chown=node:node --from=build /usr/src/app/node_modules ./node_modules 47 | COPY --chown=node:node --from=build /usr/src/app/dist ./dist 48 | 49 | ENV NODE_OPTIONS --max-old-space-size=4096 50 | 51 | CMD [ "node","--max_old_space_size=4096", "dist/main.js" ] 52 | -------------------------------------------------------------------------------- /HOW_TO_USE.md: -------------------------------------------------------------------------------- 1 | # How a Developer Can Start Using IO Flow? 2 | 3 | Welcome to IO Flow, a powerful platform for designing and automating business workflows. This README provides a step-by-step guide to help you get started with setting up Node.js, MongoDB, and running workflows with IO Flow. Follow these instructions to create, configure, and integrate workflows into your applications. 4 | 5 | ## Installation 6 | 7 | 1. **Install Node.js and MongoDB:** 8 | 9 | Start by installing Node.js and MongoDB from their official websites. Follow the installation instructions for your specific environment. 10 | 11 | 2. **Download IO Flow:** 12 | 13 | Download the IO Flow code from the official GitHub Repository: [IO Flow Repository](https://github.com/iauroSystems/io-flow-core.git). Follow the instructions to run IO Flow for your specific environment. 14 | 15 | ## Workflow Basics 16 | 17 | 3. **Learn BPMN:** 18 | 19 | Familiarize yourself with BPMN (Business Process Model and Notation) concepts and elements. BPMN forms the foundation of workflow design in IO Flow. 20 | 21 | 4. **Explore IO Flow Modeler:** 22 | 23 | Download the visual modeling tool from the Modeler in JSON format. This tool will help you create and visualize workflow diagrams. 24 | 25 | 5. **Create a Simple Workflow:** 26 | 27 | Start by creating a simple workflow model using the Modeler. Define tasks, gateways, events, and flows that make up your process. 28 | 29 | 6. **Configure and Deploy the Workflow:** 30 | 31 | Configure the IO Flow engine to run your workflow. This usually involves specifying database connections and other settings. Deploy your workflow diagram to the engine. 32 | 33 | ## Workflow Automation 34 | 35 | 7. **Use IO Flow APIs:** 36 | 37 | IO Flow provides REST and gRPC APIs. Choose the programming language you are most comfortable with and interact with the IO Flow engine using these APIs. You can manage process instances, start, complete, and monitor them. 38 | 39 | 8. **Learn IO Flow Documentation and Resources:** 40 | 41 | Explore IO Flow's comprehensive documentation and the user community forum to gain a deeper understanding of the platform. 42 | 43 | ## Advanced Workflow Customization 44 | 45 | 9. **Customize Workflows:** 46 | 47 | As you become more proficient, customize your workflows by adding user forms, gateways, and integrating with external systems. IO Flow offers connectors for this purpose. 48 | 49 | 10. **Integrate with Your Application:** 50 | 51 | Integrate IO Flow into your existing applications or systems. IO Flow provides connectors to streamline this process. 52 | 53 | 11. **Test and Debug:** 54 | 55 | Thoroughly test your workflows to ensure they work as expected. IO Flow offers features to assist with testing and debugging directly in the modeler. 56 | 57 | ## Workflow Optimization 58 | 59 | 12. **Monitor and Optimize:** 60 | 61 | Utilize IO Flow's monitoring and reporting capabilities to keep an eye on the performance and efficiency of your workflows. Make optimizations as needed. 62 | 63 | 13. **Learn Best Practices:** 64 | 65 | Stay up to date with best practices for workflow design and business process automation. IO Flow's community and resources can be valuable for this purpose. 66 | 67 | 14. **Join the Community:** 68 | 69 | IO Flow's user community is growing rapidly. Join the community, participate in forums, and seek help when needed. Sharing experiences and learning from others can be invaluable. 70 | 71 | Now you're ready to harness the power of IO Flow for designing, automating, and optimizing your business workflows. Good luck! 72 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2023] [iauro Systems](https://iauro.com/) 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IO Flow 2 | 3 | A IO Flow is a software tool or system designed to automate and manage the flow of tasks, activities, and information within an organization or system. It serves as a central platform that orchestrates and coordinates the execution of business processes or workflows. The main purpose of a IO Flow is to streamline and optimize complex, repetitive, or time-sensitive processes, improving efficiency, productivity, and accuracy. 4 | 5 | At its core, a IO Flow provides a framework for defining, modeling, and executing workflows. It typically includes the following key components: 6 | 7 | ### Workflow Design: 8 | The IO Flow allows users to create or define workflows using a graphical user interface or other modeling tools. Workflows can be represented as a series of interconnected tasks, activities, or steps, along with the associated dependencies, conditions, and rules. 9 | ### Workflow Execution: 10 | Once a workflow is defined, the engine is responsible for executing it according to the specified logic and rules. It manages the sequencing, coordination, and allocation of tasks to the appropriate individuals or systems involved in the workflow. This may involve triggering events, assigning tasks, and monitoring progress. 11 | ### Task Management: 12 | The IO Flow tracks the status and progress of individual tasks within the workflow. It ensures that tasks are assigned to the right participants, monitors their completion, and handles exceptions or errors that may occur during task execution. 13 | ### Rules and Conditions: 14 | IO Flow often incorporate rule engines or decision management systems to enable the enforcement of business rules and conditions. These rules can be used to control the flow of the workflow, make decisions, validate inputs, and ensure compliance with business policies. 15 | ### Integration: 16 | A IO Flow can integrate with various external systems, databases, or applications to gather inputs, retrieve or update data, and trigger actions. This enables seamless communication and interaction between the IO Flow and other systems involved in the workflow. 17 | ### Monitoring and Analytics: 18 | IO Flow provide real-time monitoring and reporting capabilities, allowing stakeholders to track the progress, performance, and key metrics of workflows. Analytics features may include dashboards, visualizations, and data-driven insights to identify bottlenecks, optimize processes, and make informed decisions. 19 | 20 | By employing a IO Flow, organizations can automate and streamline their business processes, reducing manual effort, minimizing errors, and enhancing operational efficiency. Workflows can be customized, adapted, and scaled as needed, enabling organizations to respond to changing requirements and improve their overall productivity and agility. 21 | 22 | ## Getting Started 23 | 24 | These instructions will get you a service up and running on your local machine for development and testing purposes. 25 | ## How to build & deploy 26 | ### Configuration 27 | Following are the environment variables required to pass for the deployment 28 | 29 | ``` 30 | NODE_ENV 31 | TCP_PORT=30009 32 | GRPC_HOST=localhost 33 | GRPC_PORT=40009 34 | MONGO_PROTOCOL=mongodb 35 | MONGO_HOST=localhost 36 | MONGO_PORT=27017 37 | MONGO_USER= 38 | MONGO_PASS= 39 | MONGO_DBNAME=workflowEngine 40 | SWAGGER_TITLE= IO Flow API Documentation 41 | SWAGGER_DESC=This documentation is only for the REST APIs 42 | SWAGGER_VERSION=1.0 43 | SWAGGER_TAG 44 | SWAGGER_BASEPATH 45 | SWAGGER_DOC_PATH=api/documentation 46 | APP_BASEPATH=v1 47 | 48 | ``` 49 | 30009 is the default port for REST APIs 50 | 40009 is the default port for gRPC APIs 51 | 52 | ### Deployment 53 | #### Local Development 54 | 55 | 1. Install Node.js: Ensure that Node.js is installed on your local machine. [How to install?](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) 56 | 57 | 2. Install Nestjs 58 | ``` 59 | npm install -g @nestjs/cli 60 | ``` 61 | 62 | 3. Install dependencies 63 | ``` 64 | npm i 65 | ``` 66 | 67 | 4. Build and Run Locally: Build your NestJS application using the command ``` npm run build ```, then start the application using ``` export NODE_ENV=development && npm run start:dev ``` or ``` npm run start:prod ```. 68 | 69 | #### Containerization (Docker) 70 | 71 | Install Docker: Install Docker on your local machine or the target server. 72 | 73 | Create Dockerfile: Create a Dockerfile in the root directory of your NestJS application. The Dockerfile specifies the necessary dependencies and commands to build your application image. 74 | 75 | Build Docker Image: Use the command docker ``` build -t my-app ``` . to build a Docker image of your NestJS application. 76 | 77 | Run Docker Container: Start a Docker container using the built image with the command ``` docker run -d -p 3000:3000 my-app ```. Adjust the port number if your application uses a different port. 78 | 79 | #### Cloud-based Deployments 80 | 81 | Cloud Platforms: Choose a cloud provider such as AWS, Azure, Google Cloud, or Heroku. 82 | 83 | Set Up Cloud Account: Create an account and set up the necessary credentials and permissions. 84 | 85 | Deploy to Cloud: Each cloud provider has its own deployment process and tools. Typically, you need to configure a deployment pipeline, define the necessary environment variables, and specify the deployment instructions (e.g., using Infrastructure as Code or cloud-specific deployment mechanisms). 86 | 87 | #### Continuous Integration and Deployment (CI/CD) 88 | 89 | CI/CD Tools: Utilize popular CI/CD tools like Jenkins, GitLab CI/CD, CircleCI, or Travis CI. 90 | 91 | Configure CI/CD Pipeline: Set up a pipeline that automatically builds and deploys your NestJS application whenever changes are pushed to the repository. Define the necessary build steps, testing, and deployment instructions. 92 | 93 | Please note that the specific steps and commands may vary depending on your environment, preferences, and deployment targets. It's always a good practice to consult the official documentation for the tools and platforms you choose to ensure accurate and up-to-date information. 94 | 95 | ## How to use IO Flow? 96 | 97 | [IO Flow Kickstart Guide](HOW_TO_USE.md) Follow these instructions to create, configure, and integrate workflows into your applications. 98 | ## Authors 99 | 100 | - [Sudhir Raut](https://github.com/sudhir-raut) 101 | 102 | ## Contributing to IO Flow 103 | 104 | Follow the [contributing guidelines](CONTRIBUTING.md) if you want to propose a change in the IO Flow core. 105 | 106 | ### Reporting Issues 107 | 108 | If you experience or witness any unacceptable behavior while interacting with our project, please report it to the project maintainers by contacting [sudhir.raut@iauro.com]. All reports will be kept confidential. 109 | 110 | ## Code of Conduct 111 | 112 | We are committed to providing a friendly, safe, and welcoming environment for all contributors and participants. Please review our [Code of Conduct](CODE_OF_CONDUCT.md) to understand our expectations for behavior. 113 | 114 | ## License 115 | 116 | IO Flow is licensed under the free License - see the [LICENSE.md](LICENSE.md) file for details 117 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | 6 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | # Mongodb service 5 | mongo_db: 6 | container_name: ioflowdb 7 | image: mongo:latest 8 | restart: always 9 | volumes: 10 | - mongo_data:/data/db 11 | 12 | # Node api service 13 | api: 14 | build: . 15 | ports: 16 | # local->container 17 | - 30009:30009 18 | environment: 19 | TCP_PORT: 30009 20 | GRPC_HOST: localhost 21 | GRPC_PORT: 40009 22 | MONGO_PROTOCOL: mongodb 23 | MONGO_HOST: mongo_db 24 | MONGO_PORT: 27017 25 | MONGO_DBNAME: workflowEngine 26 | depends_on: 27 | - mongo_db 28 | 29 | volumes: 30 | mongo_data: {} -------------------------------------------------------------------------------- /e2e/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "path": "" 3 | } -------------------------------------------------------------------------------- /envs/.env.development: -------------------------------------------------------------------------------- 1 | TCP_PORT=30009 2 | GRPC_HOST=localhost 3 | GRPC_PORT=40009 4 | 5 | MONGO_PROTOCOL=mongodb 6 | MONGO_HOST=localhost 7 | MONGO_PORT=27017 8 | MONGO_USER= 9 | MONGO_PASS= 10 | MONGO_DBNAME=workflowEngine 11 | 12 | SWAGGER_TITLE= IO Flow API Documentation 13 | SWAGGER_DESC=This documentation is only for the REST APIs 14 | SWAGGER_VERSION=1.0 15 | SWAGGER_TAG 16 | SWAGGER_BASEPATH 17 | SWAGGER_DOC_PATH=api/documentation 18 | APP_BASEPATH=v1 19 | 20 | 21 | -------------------------------------------------------------------------------- /envs/.env.local: -------------------------------------------------------------------------------- 1 | TCP_PORT=30009 2 | GRPC_HOST=localhost 3 | GRPC_PORT=40009 4 | 5 | MONGO_PROTOCOL=mongodb 6 | MONGO_HOST=localhost 7 | MONGO_PORT=27017 8 | MONGO_USER= 9 | MONGO_PASS= 10 | MONGO_DBNAME=workflowEngine 11 | 12 | 13 | SWAGGER_TITLE= IO Flow 14 | SWAGGER_DESC= Documentation for REST APIs 15 | SWAGGER_VERSION=1.0 16 | SWAGGER_TAG 17 | SWAGGER_BASEPATH 18 | SWAGGER_DOC_PATH=api/documentation 19 | APP_BASEPATH=v1 20 | -------------------------------------------------------------------------------- /examples/Employee Hiring/hiring.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Hiring Flow", 3 | "description": "Process to hiring a candidate", 4 | "key": "KEY123", 5 | "properties": [ 6 | { 7 | "key": "name", 8 | "value": { 9 | "type": "string", 10 | "default": "", 11 | "required": true 12 | } 13 | }, 14 | { 15 | "key": "job", 16 | "value": { 17 | "type": "string", 18 | "default": "", 19 | "required": true 20 | } 21 | }, 22 | { 23 | "key": "emailId", 24 | "value": { 25 | "type": "string", 26 | "default": "", 27 | "required": true 28 | } 29 | } 30 | ], 31 | "assigneeConnector": null, 32 | "stages": [ 33 | { 34 | "key": "start1", 35 | "name": "start stage", 36 | "description": "string", 37 | "type": "event", 38 | "subType": "start", 39 | "nextStages": [ 40 | "key-2" 41 | ] 42 | }, 43 | { 44 | "key": "A1", 45 | "name": "Application Recieved", 46 | "description": "Desc", 47 | "type": "activity", 48 | "subType": "system-task", 49 | "mandatory": true, 50 | "nextStages": [ 51 | "A2" 52 | ], 53 | "connector": { 54 | "type": "rest", 55 | "config": { 56 | "url": "https://reqres.in/api/users", 57 | "method": "post", 58 | "data": {} 59 | } 60 | } 61 | }, 62 | { 63 | "key": "A2", 64 | "name": "Screen Resume", 65 | "description": "Desc", 66 | "type": "activity", 67 | "subType": "user-task", 68 | "mandatory": true, 69 | "nextStages": [ 70 | "G1" 71 | ], 72 | "properties": [ 73 | { 74 | "key": "qualified", 75 | "value": { 76 | "type": "boolean", 77 | "default": false, 78 | "required": true 79 | } 80 | } 81 | ] 82 | }, 83 | { 84 | "key": "G1", 85 | "name": "Is Qalified", 86 | "description": "string", 87 | "type": "gateway", 88 | "subType": "exclusive", 89 | "nextStages": [ 90 | "end1" 91 | ], 92 | "conditions": [ 93 | { 94 | "name": "Qalified?", 95 | "op": "AND", 96 | "expressions": [ 97 | { 98 | "lhs": "$[stages..parameters.qualified]", 99 | "op": "==", 100 | "rhs": true 101 | } 102 | ], 103 | "onTrueNextStage": "A3" 104 | } 105 | ] 106 | }, 107 | { 108 | "key": "A3", 109 | "name": "Interview Round 1", 110 | "description": "Desc", 111 | "type": "activity", 112 | "subType": "user-task", 113 | "mandatory": true, 114 | "nextStages": [ 115 | "G2" 116 | ], 117 | "properties": [ 118 | { 119 | "key": "qualified", 120 | "value": { 121 | "type": "boolean", 122 | "default": false, 123 | "required": true 124 | } 125 | } 126 | ] 127 | }, 128 | { 129 | "key": "G2", 130 | "name": "Is Qalified", 131 | "description": "string", 132 | "type": "gateway", 133 | "subType": "exclusive", 134 | "nextStages": [ 135 | "end1" 136 | ], 137 | "conditions": [ 138 | { 139 | "name": "Qalified?", 140 | "op": "AND", 141 | "expressions": [ 142 | { 143 | "lhs": "$[stages..parameters.qualified]", 144 | "op": "==", 145 | "rhs": true 146 | } 147 | ], 148 | "onTrueNextStage": "A4" 149 | } 150 | ] 151 | }, 152 | { 153 | "key": "A4", 154 | "name": "Final Round", 155 | "description": "Desc", 156 | "type": "activity", 157 | "subType": "user-task", 158 | "mandatory": true, 159 | "nextStages": [ 160 | "G3" 161 | ], 162 | "properties": [ 163 | { 164 | "key": "qualified", 165 | "value": { 166 | "type": "boolean", 167 | "default": false, 168 | "required": true 169 | } 170 | } 171 | ] 172 | }, 173 | { 174 | "key": "G3", 175 | "name": "Is Qalified", 176 | "description": "string", 177 | "type": "gateway", 178 | "subType": "exclusive", 179 | "nextStages": [ 180 | "end1" 181 | ], 182 | "conditions": [ 183 | { 184 | "name": "Qalified?", 185 | "op": "AND", 186 | "expressions": [ 187 | { 188 | "lhs": "$[stages..parameters.qualified]", 189 | "op": "==", 190 | "rhs": true 191 | } 192 | ], 193 | "onTrueNextStage": "A5" 194 | } 195 | ] 196 | }, 197 | { 198 | "key": "A5", 199 | "name": "Send Offer Letter", 200 | "description": "Trigger communication", 201 | "type": "activity", 202 | "subType": "system-task", 203 | "mandatory": false, 204 | "nextStages": [ 205 | "end1" 206 | ] 207 | }, 208 | { 209 | "key": "end1", 210 | "name": "end stage", 211 | "description": "end process", 212 | "type": "event", 213 | "subType": "end" 214 | } 215 | ] 216 | } -------------------------------------------------------------------------------- /examples/Employee Hiring/hiring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iauroSystems/io-flow-core/1db91a94901a2a5a2d51adb233f15b89bcd21c56/examples/Employee Hiring/hiring.png -------------------------------------------------------------------------------- /examples/Invoice Processing/README.md: -------------------------------------------------------------------------------- 1 | # Invoice Processing Workflow 2 | 3 | This is a sample workflow designed to streamline the handling of incoming invoices from vendors or suppliers. Its purpose is to ensure that invoices are processed efficiently and in compliance with company policies. The process involves validation, approval, payment processing, and notifications. 4 | 5 | ## Image 6 | 7 | ![Workflow Image](https://drive.google.com/file/d/1huvki3Wi52hhVmut1H0XDpHsiHMJeBeS/view?usp=sharing) 8 | 9 | 10 | ## Workflow Description 11 | 12 | 1. **Start (Invoice Creation)**: The invoice is created, triggering the invoice processing workflow. 13 | 14 | 2. **Validate Invoice**: After the invoice is created, it undergoes automatic validation to ensure it meets predefined criteria. 15 | 16 | 3. **Approve / Deny**: Valid invoices are routed to a user for approval. This task requires human interaction to approve or reject the invoice. 17 | 18 | 4. **Check Status**: This gateway determines the next steps based on the approval decision. If approved, the workflow proceeds to payment processing; if rejected, it may require revision or other actions. 19 | 20 | 5. **Process Payment**: If the invoice is approved, the system processes the payment. 21 | 22 | 6. **Update In Records**: After payment processing, the system updates records for accurate accounting and reporting. 23 | 24 | 7. **Notify Approval and Denial**: These steps notify relevant stakeholders of the approval or denial outcome. 25 | 26 | 8. **End**: Represents the conclusion of the invoice processing workflow. 27 | 28 | This structured workflow ensures invoices are efficiently processed, from creation to payment, while maintaining transparency and compliance with company policies. 29 | and compliance with company policies. -------------------------------------------------------------------------------- /examples/Invoice Processing/invoice_processing.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "invoice_processing", 3 | "key": "653909c90aecd59c0bb6c244", 4 | "description": "Description", 5 | "isParallel": false, 6 | "properties": [], 7 | "criteria": { 8 | "allCompleted": true, 9 | "anyCompleted": false, 10 | "allActivitiesCompleted": true, 11 | "anyActivitiesCompleted": false, 12 | "allSuccess": false, 13 | "anySuccess": false, 14 | "onErrorComplete": true, 15 | "showError": false, 16 | "retry": false, 17 | "retries": 3, 18 | "retryInterval": 2000 19 | }, 20 | "stages": [ 21 | { 22 | "key": "event_GXSdGkD2", 23 | "name": "Start", 24 | "displayName": "Invoice Creation", 25 | "description": "The invoice is created", 26 | "type": "event", 27 | "subType": "start", 28 | "nextStages": [ 29 | "activity_fZD8WcPX" 30 | ] 31 | }, 32 | { 33 | "key": "event_L6QZAqzG", 34 | "name": "End", 35 | "displayName": "End", 36 | "description": "", 37 | "type": "event", 38 | "subType": "end" 39 | }, 40 | { 41 | "key": "activity_fZD8WcPX", 42 | "name": "Validate Invoice", 43 | "displayName": "Validate Invoice", 44 | "description": "", 45 | "type": "activity", 46 | "subType": "system-task", 47 | "nextStages": [ 48 | "activity_ItEKyCd2" 49 | ], 50 | "mandatory": false, 51 | "auto": true, 52 | "connector": { 53 | "config": { 54 | "url": "https://gessa.io/gessa-demo-service/demo?page=0&size=10", 55 | "method": "Get", 56 | "headers": {}, 57 | "timeout": 0, 58 | "data": {} 59 | }, 60 | "type": "rest" 61 | }, 62 | "properties": [] 63 | }, 64 | { 65 | "key": "activity_ItEKyCd2", 66 | "name": "Approve / Deny ", 67 | "displayName": "Approve / Deny ", 68 | "description": "", 69 | "type": "activity", 70 | "subType": "user-task", 71 | "nextStages": [ 72 | "gateway_zl0qIRbn" 73 | ], 74 | "mandatory": false, 75 | "connector": null, 76 | "customParams": { 77 | "pageId": "", 78 | "formId": "" 79 | }, 80 | "properties": [] 81 | }, 82 | { 83 | "key": "activity_OVaJ6ZX3", 84 | "name": "Notify Approval", 85 | "displayName": "Notify Approval", 86 | "description": "", 87 | "type": "activity", 88 | "subType": "system-task", 89 | "nextStages": [ 90 | "activity_fxAgWUzO" 91 | ], 92 | "mandatory": false, 93 | "auto": true, 94 | "connector": { 95 | "config": { 96 | "url": "https://gessa.io/gessa-demo-service/demo?page=0&size=10", 97 | "method": "Get", 98 | "headers": {}, 99 | "timeout": 0, 100 | "data": {} 101 | }, 102 | "type": "rest" 103 | }, 104 | "properties": [] 105 | }, 106 | { 107 | "key": "activity_2xsF9jWP", 108 | "name": "Notify Denial", 109 | "displayName": "Notify Denial", 110 | "description": "", 111 | "type": "activity", 112 | "subType": "system-task", 113 | "nextStages": [ 114 | "event_NOG8aHn0" 115 | ], 116 | "mandatory": false, 117 | "auto": true, 118 | "connector": { 119 | "config": { 120 | "url": "https://gessa.io/gessa-demo-service/demo?page=0&size=10", 121 | "method": "Get", 122 | "headers": {}, 123 | "timeout": 0, 124 | "data": {} 125 | }, 126 | "type": "rest" 127 | }, 128 | "properties": [] 129 | }, 130 | { 131 | "key": "event_NOG8aHn0", 132 | "name": "End3", 133 | "displayName": "End3", 134 | "description": "", 135 | "type": "event", 136 | "subType": "end" 137 | }, 138 | { 139 | "key": "activity_fxAgWUzO", 140 | "name": "Process Payment", 141 | "displayName": "Process Payment", 142 | "description": "", 143 | "type": "activity", 144 | "subType": "system-task", 145 | "nextStages": [ 146 | "activity_UD7eI6gL" 147 | ], 148 | "mandatory": false, 149 | "auto": true, 150 | "connector": { 151 | "config": { 152 | "url": "https://gessa.io/gessa-demo-service/demo?page=0&size=10", 153 | "method": "Get", 154 | "headers": {}, 155 | "timeout": 0, 156 | "data": {} 157 | }, 158 | "type": "rest" 159 | }, 160 | "properties": [] 161 | }, 162 | { 163 | "key": "activity_UD7eI6gL", 164 | "name": "Update In Records", 165 | "displayName": "Update In Records", 166 | "description": "", 167 | "type": "activity", 168 | "subType": "system-task", 169 | "nextStages": [ 170 | "event_L6QZAqzG" 171 | ], 172 | "mandatory": false, 173 | "auto": true, 174 | "connector": { 175 | "config": { 176 | "url": "https://gessa.io/gessa-demo-service/demo?page=0&size=10", 177 | "method": "Post", 178 | "headers": {}, 179 | "timeout": 0, 180 | "data": {} 181 | }, 182 | "type": "rest" 183 | }, 184 | "properties": [] 185 | }, 186 | { 187 | "key": "gateway_zl0qIRbn", 188 | "name": "Check Status", 189 | "displayName": "Check Status", 190 | "description": "", 191 | "type": "gateway", 192 | "subType": "exclusive", 193 | "nextStages": [ 194 | "activity_OVaJ6ZX3" 195 | ], 196 | "conditions": [ 197 | { 198 | "name": "ifApproved", 199 | "op": "and", 200 | "expressions": [ 201 | { 202 | "lhs": "true", 203 | "op": "==", 204 | "rhs": "true" 205 | } 206 | ], 207 | "onTrueNextStage": "activity_2xsF9jWP" 208 | } 209 | ] 210 | } 211 | ], 212 | "assigneeConnector": null 213 | } -------------------------------------------------------------------------------- /examples/Invoice Processing/invoice_processing_image.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iauroSystems/io-flow-core/1db91a94901a2a5a2d51adb233f15b89bcd21c56/examples/Invoice Processing/invoice_processing_image.PNG -------------------------------------------------------------------------------- /examples/Travel Reimbursement/README.md: -------------------------------------------------------------------------------- 1 | # Travel Reimbursement Workflow Example 2 | 3 | This is a sample workflow designed to simplify the process of reimbursing travel expenses incurred by employees. Its purpose is to ensure that travel expenses are efficiently reviewed, approved, and reimbursed while maintaining compliance with company policies. 4 | 5 | ![Workflow Image](workflow_image.png) 6 | 7 | ## JSON 8 | 9 | ``` 10 | 11 | { 12 | "name": "travelReimbursementWorkflow", 13 | "key": "653906530aecd59c0bb6c1ea", 14 | "description": "Description", 15 | "isParallel": false, 16 | "properties": [], 17 | "criteria": { 18 | "allCompleted": true, 19 | "anyCompleted": false, 20 | "allActivitiesCompleted": true, 21 | "anyActivitiesCompleted": false, 22 | "allSuccess": false, 23 | "anySuccess": false, 24 | "onErrorComplete": true, 25 | "showError": false, 26 | "retry": false, 27 | "retries": 3, 28 | "retryInterval": 2000 29 | }, 30 | "stages": [ 31 | { 32 | "key": "event_KkhPbZpy", 33 | "name": "Start", 34 | "displayName": "Start", 35 | "description": "", 36 | "type": "event", 37 | "subType": "start", 38 | "nextStages": [ 39 | "activity_Uh1CDYOG" 40 | ] 41 | }, 42 | { 43 | "key": "activity_Uh1CDYOG", 44 | "name": "Submit Request", 45 | "displayName": "Submit Request", 46 | "description": "Employee submits a travel expense reimbursement request, attaching all relevant receipts and documents.", 47 | "type": "activity", 48 | "subType": "user-task", 49 | "nextStages": [ 50 | "gateway_erygmG14" 51 | ], 52 | "mandatory": false, 53 | "connector": null, 54 | "customParams": { 55 | "pageId": "", 56 | "formId": "" 57 | }, 58 | "properties": [] 59 | }, 60 | { 61 | "key": "gateway_erygmG14", 62 | "name": "Approval Required?", 63 | "displayName": "Approval Required?", 64 | "description": "", 65 | "type": "gateway", 66 | "subType": "exclusive", 67 | "nextStages": [ 68 | "activity_j9ilQCEg" 69 | ], 70 | "conditions": [ 71 | { 72 | "name": "Manager Approval", 73 | "op": "and", 74 | "expressions": [ 75 | { 76 | "lhs": "input", 77 | "op": "==", 78 | "rhs": "true" 79 | } 80 | ], 81 | "onTrueNextStage": "activity_7WvUGL6l" 82 | } 83 | ] 84 | }, 85 | { 86 | "key": "activity_7WvUGL6l", 87 | "name": "Manager Approval", 88 | "displayName": "Manager Approval", 89 | "description": "Manager reviews the reimbursement request and approves or rejects it.", 90 | "type": "activity", 91 | "subType": "user-task", 92 | "nextStages": [ 93 | "gateway_awFDW3v9" 94 | ], 95 | "mandatory": false, 96 | "connector": null, 97 | "customParams": { 98 | "pageId": "", 99 | "formId": "" 100 | }, 101 | "properties": [] 102 | }, 103 | { 104 | "key": "activity_j9ilQCEg", 105 | "name": "Process Payment", 106 | "displayName": "Process Payment", 107 | "description": "The system processes the approved reimbursement request and prepares the payment.", 108 | "type": "activity", 109 | "subType": "system-task", 110 | "nextStages": [ 111 | "activity_i5vUYDS1" 112 | ], 113 | "mandatory": false, 114 | "auto": false, 115 | "connector": null, 116 | "properties": [] 117 | }, 118 | { 119 | "key": "activity_i5vUYDS1", 120 | "name": "Payment Notification", 121 | "displayName": "Payment Notification", 122 | "description": "The employee receives a notification and confirms the payment receipt.", 123 | "type": "activity", 124 | "subType": "user-task", 125 | "nextStages": [ 126 | "event_7aPtDeS2" 127 | ], 128 | "mandatory": false, 129 | "connector": null, 130 | "customParams": { 131 | "pageId": "", 132 | "formId": "" 133 | }, 134 | "properties": [] 135 | }, 136 | { 137 | "key": "event_SV6bTK7T", 138 | "name": "End", 139 | "displayName": "End", 140 | "description": "", 141 | "type": "event", 142 | "subType": "end" 143 | }, 144 | { 145 | "key": "gateway_awFDW3v9", 146 | "name": "Manager Decision", 147 | "displayName": "Manager Decision", 148 | "description": "", 149 | "type": "gateway", 150 | "subType": "exclusive", 151 | "nextStages": [ 152 | "activity_j9ilQCEg" 153 | ], 154 | "conditions": [ 155 | { 156 | "name": "if_rejected", 157 | "op": "and", 158 | "expressions": [ 159 | { 160 | "lhs": "true", 161 | "op": "==", 162 | "rhs": "true" 163 | } 164 | ], 165 | "onTrueNextStage": "event_SV6bTK7T" 166 | } 167 | ] 168 | }, 169 | { 170 | "key": "event_7aPtDeS2", 171 | "name": "End2", 172 | "displayName": "End2", 173 | "description": "", 174 | "type": "event", 175 | "subType": "end" 176 | } 177 | ], 178 | "assigneeConnector": null 179 | } 180 | 181 | ``` 182 | 183 | 184 | ## Workflow Description 185 | 186 | 1. **Start (Submit Request)**: The employee submits a travel expense reimbursement request, attaching all relevant receipts and documents. 187 | 188 | 2. **Approval Required?**: This gateway checks whether manager approval is required. 189 | 190 | 3. **Manager Approval**: If manager approval is required, the manager reviews the reimbursement request and approves or rejects it. 191 | 192 | 4. **Manager Decision**: This gateway determines the next steps based on the manager's decision. If rejected, the workflow proceeds to the end. 193 | 194 | 5. **Process Payment**: The system processes the approved reimbursement request and prepares the payment. 195 | 196 | 6. **Payment Notification**: The employee receives a notification and confirms the payment receipt. 197 | 198 | 7. **End**: Represents the conclusion of the travel reimbursement workflow. 199 | 200 | This structured workflow ensures that travel expenses are efficiently processed, from submission to payment receipt, while adhering to company policies and ensuring transparency. -------------------------------------------------------------------------------- /examples/Travel Reimbursement/travel_reimbursement.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "travelReimbursementWorkflow", 3 | "key": "653906530aecd59c0bb6c1ea", 4 | "description": "Description", 5 | "isParallel": false, 6 | "properties": [], 7 | "criteria": { 8 | "allCompleted": true, 9 | "anyCompleted": false, 10 | "allActivitiesCompleted": true, 11 | "anyActivitiesCompleted": false, 12 | "allSuccess": false, 13 | "anySuccess": false, 14 | "onErrorComplete": true, 15 | "showError": false, 16 | "retry": false, 17 | "retries": 3, 18 | "retryInterval": 2000 19 | }, 20 | "stages": [ 21 | { 22 | "key": "event_KkhPbZpy", 23 | "name": "Start", 24 | "displayName": "Start", 25 | "description": "", 26 | "type": "event", 27 | "subType": "start", 28 | "nextStages": [ 29 | "activity_Uh1CDYOG" 30 | ] 31 | }, 32 | { 33 | "key": "activity_Uh1CDYOG", 34 | "name": "Submit Request", 35 | "displayName": "Submit Request", 36 | "description": "Employee submits a travel expense reimbursement request, attaching all relevant receipts and documents.", 37 | "type": "activity", 38 | "subType": "user-task", 39 | "nextStages": [ 40 | "gateway_erygmG14" 41 | ], 42 | "mandatory": false, 43 | "connector": null, 44 | "customParams": { 45 | "pageId": "", 46 | "formId": "" 47 | }, 48 | "properties": [] 49 | }, 50 | { 51 | "key": "gateway_erygmG14", 52 | "name": "Approval Required?", 53 | "displayName": "Approval Required?", 54 | "description": "", 55 | "type": "gateway", 56 | "subType": "exclusive", 57 | "nextStages": [ 58 | "activity_j9ilQCEg" 59 | ], 60 | "conditions": [ 61 | { 62 | "name": "Manager Approval", 63 | "op": "and", 64 | "expressions": [ 65 | { 66 | "lhs": "input", 67 | "op": "==", 68 | "rhs": "true" 69 | } 70 | ], 71 | "onTrueNextStage": "activity_7WvUGL6l" 72 | } 73 | ] 74 | }, 75 | { 76 | "key": "activity_7WvUGL6l", 77 | "name": "Manager Approval", 78 | "displayName": "Manager Approval", 79 | "description": "Manager reviews the reimbursement request and approves or rejects it.", 80 | "type": "activity", 81 | "subType": "user-task", 82 | "nextStages": [ 83 | "gateway_awFDW3v9" 84 | ], 85 | "mandatory": false, 86 | "connector": null, 87 | "customParams": { 88 | "pageId": "", 89 | "formId": "" 90 | }, 91 | "properties": [] 92 | }, 93 | { 94 | "key": "activity_j9ilQCEg", 95 | "name": "Process Payment", 96 | "displayName": "Process Payment", 97 | "description": "The system processes the approved reimbursement request and prepares the payment.", 98 | "type": "activity", 99 | "subType": "system-task", 100 | "nextStages": [ 101 | "activity_i5vUYDS1" 102 | ], 103 | "mandatory": false, 104 | "auto": false, 105 | "connector": null, 106 | "properties": [] 107 | }, 108 | { 109 | "key": "activity_i5vUYDS1", 110 | "name": "Payment Notification", 111 | "displayName": "Payment Notification", 112 | "description": "The employee receives a notification and confirms the payment receipt.", 113 | "type": "activity", 114 | "subType": "user-task", 115 | "nextStages": [ 116 | "event_7aPtDeS2" 117 | ], 118 | "mandatory": false, 119 | "connector": null, 120 | "customParams": { 121 | "pageId": "", 122 | "formId": "" 123 | }, 124 | "properties": [] 125 | }, 126 | { 127 | "key": "event_SV6bTK7T", 128 | "name": "End", 129 | "displayName": "End", 130 | "description": "", 131 | "type": "event", 132 | "subType": "end" 133 | }, 134 | { 135 | "key": "gateway_awFDW3v9", 136 | "name": "Manager Decision", 137 | "displayName": "Manager Decision", 138 | "description": "", 139 | "type": "gateway", 140 | "subType": "exclusive", 141 | "nextStages": [ 142 | "activity_j9ilQCEg" 143 | ], 144 | "conditions": [ 145 | { 146 | "name": "if_rejected", 147 | "op": "and", 148 | "expressions": [ 149 | { 150 | "lhs": "true", 151 | "op": "==", 152 | "rhs": "true" 153 | } 154 | ], 155 | "onTrueNextStage": "event_SV6bTK7T" 156 | } 157 | ] 158 | }, 159 | { 160 | "key": "event_7aPtDeS2", 161 | "name": "End2", 162 | "displayName": "End2", 163 | "description": "", 164 | "type": "event", 165 | "subType": "end" 166 | } 167 | ], 168 | "assigneeConnector": null 169 | } -------------------------------------------------------------------------------- /examples/Travel Reimbursement/travel_reimbursement_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iauroSystems/io-flow-core/1db91a94901a2a5a2d51adb233f15b89bcd21c56/examples/Travel Reimbursement/travel_reimbursement_image.png -------------------------------------------------------------------------------- /nano.save: -------------------------------------------------------------------------------- 1 | Added : integrated timer completion feature 2 | 3 | cron job scheduled to complete estimatedTimeDuration of timer node 4 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src", 4 | "compilerOptions": { 5 | "assets": [ 6 | { 7 | "include": "./modules/**/proto/*.proto", 8 | "outDir": "./dist/src" 9 | }, 10 | { 11 | "include": "./shared/clients/**/*.proto", 12 | "outDir": "./dist/src" 13 | }, 14 | { 15 | "include": "./proto/**/*.proto", 16 | "outDir": "./dist/src" 17 | } 18 | ] 19 | } 20 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "io-flow", 3 | "version": "1.0.0", 4 | "description": "IO Flow is a software tool or system designed to automate and manage the flow of tasks, activities, and information within an organization or system. It serves as a central platform that orchestrates and coordinates the execution of business processes or workflows. The main purpose of a IO Flow is to streamline and optimize complex, repetitive, or time-sensitive processes, improving efficiency, productivity, and accuracy.", 5 | "author": { 6 | "name": "Sudhir Raut", 7 | "email": "sudhir.raut@iauro.com", 8 | "url": "https://github.com/sudhir-raut" 9 | }, 10 | "private": false, 11 | "license": "SEE LICENSE IN LICENSE.md", 12 | "keywords": [ 13 | "io flow", 14 | "workflow", 15 | "iauro", 16 | "engine", 17 | "bpmn" 18 | ], 19 | "homepage": "https://github.com/iauroSystems/io-flow-core", 20 | "bugs": { 21 | "url": "https://github.com/iauroSystems/io-flow-core/issues", 22 | "email": "sudhir.raut@iauro.com" 23 | }, 24 | "files": [ 25 | "package.json", 26 | "README.md", 27 | "LICENSE.md", 28 | "main.ts" 29 | ], 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/iauroSystems/io-flow-core.git" 33 | }, 34 | "scripts": { 35 | "prebuild": "rimraf dist", 36 | "build": "nest build", 37 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 38 | "start": "nest start --watch", 39 | "start:any": "nest start --watch", 40 | "start:dev": "webpack --config webpack.config.js --watch", 41 | "start:debug": "nest start --debug --watch", 42 | "start:prod": "node --max_old_space_size=1024 dist/main", 43 | "test": "jest", 44 | "test:watch": "jest --watch", 45 | "test:cov": "jest --coverage", 46 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 47 | "test:e2e": "jest --config ./test/jest-e2e.json", 48 | "sonar": "node sonar-properties.js" 49 | }, 50 | "dependencies": { 51 | "@grpc/grpc-js": "^1.9.2", 52 | "@grpc/proto-loader": "^0.7.9", 53 | "@nestcloud/grpc": "^0.7.17", 54 | "@nestjs/axios": "^3.0.0", 55 | "@nestjs/common": "^10.2.4", 56 | "@nestjs/config": "^3.0.1", 57 | "@nestjs/core": "^10.2.4", 58 | "@nestjs/microservices": "^10.2.4", 59 | "@nestjs/mongoose": "^10.0.1", 60 | "@nestjs/platform-express": "^10.2.4", 61 | "@nestjs/schedule": "^3.0.3", 62 | "@nestjs/swagger": "^7.1.10", 63 | "@types/cron": "^2.0.1", 64 | "axios": "^1.5.0", 65 | "class-transformer": "^0.5.1", 66 | "class-validator": "^0.14.0", 67 | "cluster": "^0.7.7", 68 | "expressionparser": "^1.1.5", 69 | "joi": "^17.10.1", 70 | "jwt-decode": "^3.1.2", 71 | "lodash": "^4.17.21", 72 | "moment": "^2.29.4", 73 | "mongoose": "^7.5.0", 74 | "openai": "^4.6.0", 75 | "reflect-metadata": "^0.1.13", 76 | "rimraf": "^5.0.1", 77 | "rxjs": "^7.8.1", 78 | "sanitize-filename": "^1.6.3", 79 | "swagger-ui-express": "^5.0.0", 80 | "xml-js": "^1.6.11" 81 | }, 82 | "devDependencies": { 83 | "@nestjs/cli": "^10.1.17", 84 | "@nestjs/schematics": "^10.0.2", 85 | "@nestjs/testing": "^10.2.4", 86 | "@types/express": "^4.17.17", 87 | "@types/jest": "29.5.4", 88 | "@types/multer": "^1.4.7", 89 | "@types/node": "^20.6.0", 90 | "@types/supertest": "^2.0.12", 91 | "@typescript-eslint/eslint-plugin": "^6.6.0", 92 | "@typescript-eslint/parser": "^6.6.0", 93 | "eslint": "^8.49.0", 94 | "eslint-config-prettier": "^9.0.0", 95 | "eslint-plugin-prettier": "^5.0.0", 96 | "jest": "^29.6.4", 97 | "prettier": "^3.0.3", 98 | "source-map-support": "^0.5.21", 99 | "supertest": "^6.3.3", 100 | "ts-jest": "^29.1.1", 101 | "ts-loader": "^9.4.4", 102 | "ts-node": "^10.9.1", 103 | "tsconfig-paths": "^4.2.0", 104 | "typescript": "^5.2.2" 105 | }, 106 | "jest": { 107 | "moduleFileExtensions": [ 108 | "js", 109 | "json", 110 | "ts" 111 | ], 112 | "rootDir": "src", 113 | "testRegex": ".*\\.spec\\.ts$", 114 | "transform": { 115 | "^.+\\.(t|j)s$": "ts-jest" 116 | }, 117 | "collectCoverageFrom": [ 118 | "**/*.(t|j)s" 119 | ], 120 | "coverageDirectory": "../coverage", 121 | "testEnvironment": "node" 122 | }, 123 | "engines": { 124 | "node": ">= v18.13.0", 125 | "npm": ">= v6.13.6" 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | # Required metadata 2 | sonar.projectKey=io-flow 3 | sonar.projectName=io-flow 4 | sonar.projectVersion=1.0 5 | 6 | # Comma-separated paths to directories with sources (required) 7 | sonar.sources=src 8 | sonar.exclusions=node_modules/**/* 9 | # Language 10 | sonar.language=js 11 | sonar.javascript.lcov.reportPaths=coverage/lcov.info 12 | sonar.javascript.lcov.itReportPath=coverage/lcov.info 13 | 14 | # Encoding of the source files 15 | sonar.sourceEncoding=UTF-8 16 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | // require('dotenv').config({ path: `envs/.env.${process.env.NODE_ENV}` }) 2 | 3 | import { MiddlewareConsumer, Module } from '@nestjs/common' 4 | import { ConfigModule } from '@nestjs/config' 5 | import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core'; 6 | import { AllExceptionsFilter } from './common/exceptions'; 7 | import { HttpRequestHeadersMiddleware } from './common/middlewares'; 8 | import { ResponseInterceptor } from './common/interceptors'; 9 | import { HttpModule } from '@nestjs/axios'; 10 | import { ProcessDefinitionsModule, ProcessInstancesModule, TasksModule } from './modules'; 11 | import { HttpConnector } from 'src/shared/connectors'; 12 | import { MongooseModule } from '@nestjs/mongoose'; 13 | 14 | // example run command 15 | // export NODE_ENV=local && npm start 16 | 17 | // hot reload with env 18 | // export NODE_ENV=local && npm run start:any 19 | // hot reload 20 | // npm run start:dev 21 | const MONGO_PROTOCOL = process.env.MONGO_PROTOCOL, MONGO_HOST = process.env.MONGO_HOST, MONGO_PORT = process.env.MONGO_PORT, MONGO_USER = process.env.MONGO_USER, MONGO_PASS = process.env.MONGO_PASS, MONGO_DBNAME = process.env.MONGO_DBNAME || 'tenancy', MONGO_CLUSTER_HOSTS = process.env.MONGO_CLUSTER_HOSTS, 22 | MONGO_REPLICA_SET = process.env.MONGO_REPLICA_SET; 23 | 24 | @Module({ 25 | imports: [ 26 | ConfigModule.forRoot({ 27 | isGlobal: true, 28 | envFilePath: `envs/.env.${process.env.NODE_ENV}`, 29 | }), 30 | HttpModule, 31 | MongooseModule.forRoot(`mongodb://${MONGO_HOST}:${MONGO_PORT}/${MONGO_DBNAME}`), 32 | ProcessDefinitionsModule, 33 | ProcessInstancesModule, 34 | TasksModule, 35 | ], 36 | controllers: [], 37 | providers: [ 38 | { provide: APP_FILTER, useClass: AllExceptionsFilter }, 39 | { provide: APP_INTERCEPTOR, useClass: ResponseInterceptor }, 40 | HttpConnector, 41 | HttpRequestHeadersMiddleware 42 | ], 43 | }) 44 | export class AppModule { 45 | // Global Middleware, Inbound logging 46 | public configure(consumer: MiddlewareConsumer): void { 47 | consumer.apply(HttpRequestHeadersMiddleware).forRoutes('*'); 48 | // consumer.apply(AppLoggerMiddleware).forRoutes('*'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/common/const/BPMN-mappings.ts: -------------------------------------------------------------------------------- 1 | export class Mappings { 2 | static StageMappings = { 3 | 'bpmn:startEvent': { 4 | key: '', 5 | name: '', 6 | description: '', 7 | type: 'event', 8 | subType: 'start', 9 | nextStages: [] 10 | }, 11 | 'bpmn:endEvent': { 12 | key: '', 13 | name: '', 14 | description: '', 15 | type: 'event', 16 | subType: 'end', 17 | nextStages: [] 18 | }, 19 | 'bpmn:intermediateCatchEvent': { 20 | key: '', 21 | name: 'Timer', 22 | description: 'timer', 23 | type: 'event', 24 | subType: 'timer', 25 | estimatedTimeDuration: 0, 26 | nextStages: [] 27 | }, 28 | 'bpmn:exclusiveGateway': { 29 | key: '', 30 | name: '', 31 | description: '', 32 | type: 'gateway', 33 | subType: 'exclusive', 34 | nextStages: [] 35 | }, 36 | 'bpmn:parallelGateway': { 37 | key: '', 38 | name: '', 39 | description: '', 40 | type: 'gateway', 41 | subType: 'parallel', 42 | nextStages: [] 43 | }, 44 | 'bpmn:eventBasedGateway': { 45 | key: '', 46 | name: '', 47 | description: '', 48 | type: 'gateway', 49 | subType: 'timer', 50 | nextStages: [] 51 | }, 52 | 'bpmn:task': { 53 | key: '', 54 | name: '', 55 | description: '', 56 | type: 'activity', 57 | subType: 'task', 58 | auto: true, 59 | mandatory: true, 60 | nextStages: [], 61 | criteria: null, 62 | connector: null 63 | }, 64 | 'bpmn:sendTask': { 65 | key: '', 66 | name: '', 67 | description: '', 68 | type: 'activity', 69 | subType: 'send-task', 70 | auto: true, 71 | mandatory: true, 72 | nextStages: [], 73 | criteria: null, 74 | connector: null 75 | }, 76 | 'bpmn:receiveTask': { 77 | key: '', 78 | name: '', 79 | description: '', 80 | type: 'activity', 81 | subType: 'receive-task', 82 | auto: true, 83 | mandatory: true, 84 | nextStages: [], 85 | criteria: null, 86 | connector: null 87 | }, 88 | 'bpmn:userTask': { 89 | key: '', 90 | name: '', 91 | description: '', 92 | type: 'activity', 93 | mandatory: true, 94 | subType: 'user-task', 95 | nextStages: [], 96 | properties: [], 97 | assignee: '' 98 | 99 | }, 100 | 'bpmn:manualTask': { 101 | key: '', 102 | name: '', 103 | description: '', 104 | type: 'activity', 105 | subType: 'manual-task', 106 | auto: true, 107 | mandatory: true, 108 | nextStages: [], 109 | criteria: null, 110 | connector: null 111 | }, 112 | 'bpmn:businessRuleTask': { 113 | key: '', 114 | name: '', 115 | description: '', 116 | type: 'activity', 117 | subType: 'business-rule-task', 118 | auto: true, 119 | mandatory: true, 120 | nextStages: [], 121 | criteria: null, 122 | connector: null 123 | }, 124 | 'bpmn:serviceTask': { 125 | key: '', 126 | name: '', 127 | description: '', 128 | type: 'activity', 129 | subType: 'service-task', 130 | auto: true, 131 | mandatory: true, 132 | nextStages: [], 133 | criteria: null, 134 | connector: null 135 | }, 136 | 'bpmn:scriptTask': { 137 | key: '', 138 | name: '', 139 | description: '', 140 | type: 'activity', 141 | subType: 'script-task', 142 | auto: true, 143 | mandatory: true, 144 | nextStages: [], 145 | criteria: null, 146 | connector: null 147 | }, 148 | 'bpmn:callActivity': { 149 | key: '', 150 | name: '', 151 | description: '', 152 | type: 'activity', 153 | subType: 'call-activity', 154 | auto: true, 155 | mandatory: true, 156 | nextStages: [], 157 | criteria: null, 158 | connector: null, 159 | processDefinitionKey: '' 160 | }, 161 | 'bpmn:subProcess': { 162 | key: '', 163 | name: '', 164 | description: '', 165 | type: 'activity', 166 | subType: 'sub-process', 167 | auto: true, 168 | mandatory: true, 169 | nextStages: [], 170 | criteria: null, 171 | connector: null 172 | }, 173 | 174 | 175 | 176 | } 177 | } -------------------------------------------------------------------------------- /src/common/const/constants.ts: -------------------------------------------------------------------------------- 1 | export class Constants { 2 | static STAGE_TYPES = { 3 | EVENT: 'event', 4 | ACTIVITY: 'activity', 5 | GATEWAY: 'gateway' 6 | } 7 | static STAGE_SUB_TYPES = { 8 | START: 'start', 9 | END: 'end', 10 | TIMER: 'timer', 11 | TASK: 'task', 12 | USER_TASK: 'user-task', 13 | SYSTEM_TASK: 'system-task', 14 | COMPOUND_TASK: 'compound-task', 15 | SEND_TASK: 'send-task', 16 | RECEIVE_TASK: 'receive-task', 17 | MANUAL_TASK: 'manual-task', 18 | BUSINESS_RULE_TASK: 'business-rule-task', 19 | SERVICE_TASK: 'service-task', 20 | SCRIPT_TASK: 'script-task', 21 | CALL_ACTIVITY: 'call-activity', 22 | SUB_PROCESS: 'sub-process', 23 | IF_ELSE: 'if-else', 24 | SWITCH_CASE: 'switch-case', 25 | EXCLUSIVE: 'exclusive', 26 | PARALLEL: 'parallel', 27 | INCLUSIVE: 'inclusive', 28 | EVENT_BASED: 'event-based', 29 | } 30 | static STAGE_STATUSES = { 31 | WAITING: 'waiting', 32 | ACTIVE: 'active', 33 | COMPLETED: 'completed', 34 | ERROR: 'error', 35 | STARTED: 'started', 36 | ON_HOLD: 'on-hold', 37 | CANCELLED: 'cancelled', 38 | RUNNING: 'running' 39 | } 40 | } 41 | 42 | export class Paths { 43 | // static WORKFLOW_PROTO_DIR = 'src/shared/clients'; 44 | static BPMN_XML = 'src/shared/bpmn'; 45 | static PUBLIC = 'src/shared/public' 46 | static WORKFLOW_PROTO_DIR = 'src/proto/' 47 | } 48 | 49 | export class LogEntities { 50 | static PROCESS_DEF = 'process-definitions'; 51 | static PROCESS_INST = 'process-instances' 52 | } 53 | 54 | export class Operations { 55 | static CREATE = 'create'; 56 | static UPDATE = 'update'; 57 | static DELETE = 'delete'; 58 | } 59 | 60 | export class CronJobs { 61 | static TIMER = 'timer'; 62 | } -------------------------------------------------------------------------------- /src/common/const/custom-messages.ts: -------------------------------------------------------------------------------- 1 | export class CustomMessages { 2 | static WORKFLOW_CREATED = 'workflow created successfully'; 3 | 4 | static WORKFLOW_CREATED_NEW_VERSION = 'workflow for this key is already present, created with the new version'; 5 | 6 | static WORKFLOW_UPDATED = 'workflow updated successfully'; 7 | 8 | static WORKFLOW_NOT_FOUND = 'workflow not found'; 9 | 10 | static WORKFLOW_EXISTS = 'Workflow already exists'; 11 | 12 | static WORKFLOW_NOT_EXISTS = 'Workflow does not exists'; 13 | 14 | static UNAUTHORIZED_ACTIVITY = 'You are not authorized to perform this activity'; 15 | 16 | static SUCCESS = 'success'; 17 | 18 | static FAILURE = 'failure'; 19 | 20 | static BAD_REQUEST = 'Input data is not correct'; 21 | 22 | static WORKFLOW_ENDED = 'Workflow ended successfully'; 23 | 24 | static START_STAGE_NOT_PRESENT = 'Start event not found'; 25 | 26 | static PROCESS_INSTANCE_EXISTS = 'Process instance already exists'; 27 | 28 | static PROCESS_INSTANCE_NOT_EXISTS = 'Process instance does not exists'; 29 | 30 | static PROCESS_INSTANCE_NOT_FOUND = 'Process instance not found'; 31 | 32 | static PROCESS_INSTANCE_CREATED = 'Process instance created'; 33 | 34 | static PROCESS_INSTANCE_STARTED = 'Process instance started'; 35 | 36 | static PROCESS_INSTANCE_RUNNING = 'Process instance is running'; 37 | 38 | static CHILD_PROCESS_INSTANCE_NOT_COMPLETED = 'Child process instance not completed'; 39 | 40 | static TASK_ALREADY_COMPLETED = 'Task is already completed'; 41 | 42 | static TASK_NOT_ACTIVE = 'Task is not active'; 43 | 44 | static FLOW_NOT_ACTIVE = 'Instance is not active'; 45 | 46 | static TASK_UPDATED = 'Task updated successfully'; 47 | 48 | static NEXT_STAGE_NOT_AVAILABLE = 'Next stage is not available'; 49 | 50 | static INVALID_JSON = 'JSON is invalid'; 51 | 52 | static FLOW_NOT_ON_HOLD = 'Flow is not on-hold'; 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/common/const/enums.ts: -------------------------------------------------------------------------------- 1 | export enum StageSubTypes { 2 | START = 'start', 3 | END = 'end', 4 | TIMER = 'timer', 5 | INTERMEDIATE_EVENT = 'intermediate-event', 6 | TASK = 'task', 7 | USER_TASK = 'user-task', 8 | SYSTEM_TASK = 'system-task', 9 | COMPOUND_TASK = 'compound-task', 10 | SEND_TASK = 'send-task', 11 | RECEIVE_TASK = 'receive-task', 12 | MANUAL_TASK = 'manual-task', 13 | BUSINESS_RULE_TASK = 'business-rule-task', 14 | SERVICE_TASK = 'service-task', 15 | SCRIPT_TASK = 'script-task', 16 | CALL_ACTIVITY = 'call-activity', 17 | SUB_PROCESS = 'sub-process', 18 | EXCLUSIVE = 'exclusive', 19 | PARALLEL = 'parallel', 20 | INCLUSIVE = 'inclusive', 21 | IF_ELSE = 'if-else', 22 | SWITCH_CASE = 'switch-case', 23 | } 24 | 25 | export enum StageTypes { 26 | EVENT = 'event', 27 | ACTIVITY = 'activity', 28 | GATEWAY = 'gateway' 29 | } 30 | 31 | 32 | export enum ConnectorTypes { 33 | REST = 'rest', 34 | GRPC = 'grpc' 35 | } 36 | 37 | export enum Webhooks { 38 | CREATE_INSTANCE = 'instance:create', 39 | UPDATE_INSTANCE = 'instance:update', 40 | DELETE_INSTANCE = 'instance:delete', 41 | CREATE_TASK = 'task:create', 42 | UPDATE_TASK = 'task:update', 43 | DELETE_TASK = 'task:delete' 44 | } 45 | 46 | export enum InputStatuses { 47 | COMPLETE = 'complete', 48 | HOLD = 'hold', 49 | CANCEL = 'cancel', 50 | RESUME = 'resume', 51 | EMPTY = '' 52 | } -------------------------------------------------------------------------------- /src/common/exceptions/custom-error.ts: -------------------------------------------------------------------------------- 1 | export default class CustomError extends Error { 2 | statusCode = 500; 3 | timestamp = new Date().toISOString(); 4 | error: any 5 | constructor(statusCode: number, message: string, error?: any) { 6 | super(); 7 | this.statusCode = statusCode; 8 | this.message = message; 9 | this.error = error; 10 | // Object.setPrototypeOf(this, CustomError.prototype); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/common/exceptions/http-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExceptionFilter, 3 | Catch, 4 | ArgumentsHost, 5 | HttpException, 6 | HttpStatus, 7 | Logger, 8 | } from '@nestjs/common'; 9 | import { HttpAdapterHost } from '@nestjs/core'; 10 | import CustomError from './custom-error'; 11 | 12 | @Catch() 13 | export class AllExceptionsFilter implements ExceptionFilter { 14 | constructor (private readonly httpAdapterHost: HttpAdapterHost) { } 15 | 16 | private readonly logger: Logger = new Logger(); 17 | 18 | catch(exception: unknown, host: ArgumentsHost): void { 19 | // In certain situations `httpAdapter` might not be available in the 20 | // constructor method, thus we should resolve it here. 21 | const { httpAdapter } = this.httpAdapterHost; 22 | let httpStatus = 500; 23 | let message = 'Internal server error'; 24 | let exceptionResponse = null; 25 | const ctx = host.switchToHttp(); 26 | const request = ctx.getRequest(); 27 | if (exception instanceof HttpException) { 28 | httpStatus = exception.getStatus() || HttpStatus.INTERNAL_SERVER_ERROR; 29 | message = exception.message || message; 30 | exceptionResponse = exception.getResponse(); 31 | } else if (exception instanceof CustomError) { 32 | httpStatus = exception.statusCode || HttpStatus.INTERNAL_SERVER_ERROR; 33 | message = exception.message || message; 34 | exceptionResponse = { error: exception.error }; 35 | // this.logger.error(`${exception.message}: message`, exception.stack); 36 | } else { 37 | // Error Notifications 38 | httpStatus = exception['status'] || exception['statusCode'] || HttpStatus.INTERNAL_SERVER_ERROR; 39 | message = exception['message'] || message 40 | 41 | // this.logger.error('UnhandledException', exception); 42 | } 43 | const responseBody = { 44 | ...{ 45 | timestamp: new Date().toISOString(), 46 | statusCode: httpStatus, 47 | message 48 | }, 49 | ...exceptionResponse 50 | } 51 | const { ip, method, originalUrl } = request; 52 | const userAgent = request.get('user-agent') || ''; 53 | const tenantId = request.get('x-tenant-id') || ''; 54 | 55 | this.logger.error(`[${tenantId}]: ${method} ${originalUrl} --> ${httpStatus} - ${userAgent} ${ip}: ${JSON.stringify(exception)}`); 56 | httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus); 57 | } 58 | } -------------------------------------------------------------------------------- /src/common/exceptions/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './custom-error' 2 | export * from './http-exception.filter' -------------------------------------------------------------------------------- /src/common/guards/rbac.interface.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from "rxjs"; 2 | 3 | 4 | export interface RbacGrpcInterface { 5 | checkAccess(data: { data: any }): Observable 6 | } -------------------------------------------------------------------------------- /src/common/interceptors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './response.interceptor' -------------------------------------------------------------------------------- /src/common/interceptors/response.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, 3 | ExecutionContext, 4 | HttpStatus, 5 | Injectable, 6 | NestInterceptor, 7 | Logger 8 | } from '@nestjs/common'; 9 | import { Observable } from 'rxjs'; 10 | import { map } from 'rxjs/operators'; 11 | 12 | import { Request, Response, NextFunction } from 'express'; 13 | 14 | @Injectable() 15 | export class ResponseInterceptor implements NestInterceptor { 16 | private logger = new Logger('HTTP'); 17 | 18 | intercept(context: ExecutionContext, next: CallHandler): Observable { 19 | const request = context.switchToHttp().getRequest(); 20 | const response = context.switchToHttp().getResponse(); 21 | 22 | const { ip, method, originalUrl, body } = request; 23 | const userAgent = request.get('user-agent') || ''; 24 | response.on('close', () => { 25 | const { statusCode } = response; 26 | if (statusCode < 400 && originalUrl !== '/v1/timer') { 27 | const contentLength = response.get('content-length'); 28 | this.logger.log(`${method} ${originalUrl} --> ${statusCode} ${contentLength} - ${userAgent} ${ip}`); 29 | } 30 | }); 31 | // if (response.status === 201) 32 | return next.handle() 33 | // .pipe( 34 | // map(data => { 35 | // return response.status(data.statusCode !== undefined ? data.statusCode : HttpStatus.OK).json(data); 36 | // }), 37 | // ); 38 | } 39 | } -------------------------------------------------------------------------------- /src/common/middlewares/http-request-headers.middleware.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/dot-notation */ 2 | 3 | import { Injectable, NestMiddleware } from '@nestjs/common'; 4 | import { Request, Response, NextFunction } from 'express'; 5 | import jwt_decode from 'jwt-decode' 6 | @Injectable() 7 | export class HttpRequestHeadersMiddleware implements NestMiddleware { 8 | 9 | async use(request: Request, _response: Response, next: NextFunction) { 10 | let tenantId = request.headers['x-tenant-id'] || request.headers['X-TENANT-ID']; 11 | 12 | if (request.headers['authorization']) { 13 | const token = request.headers['authorization'].split(' ')[1] 14 | const decodedToken = jwt_decode(token); 15 | tenantId = decodedToken['tenantId'] || tenantId; 16 | request.headers['x-tenant-id'] = tenantId; 17 | request.headers['user-id'] = decodedToken['userId'] || request.headers['user-id']; 18 | } 19 | return next(); 20 | } 21 | } -------------------------------------------------------------------------------- /src/common/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export * from './logger.middleware'; 2 | export * from './http-request-headers.middleware' -------------------------------------------------------------------------------- /src/common/middlewares/logger.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NestMiddleware, Logger } from '@nestjs/common'; 2 | 3 | import { Request, Response, NextFunction } from 'express'; 4 | 5 | @Injectable() 6 | export class AppLoggerMiddleware implements NestMiddleware { 7 | private logger = new Logger('HTTP'); 8 | 9 | use(request: Request, response: Response, next: NextFunction): void { 10 | const { ip, method, originalUrl } = request; 11 | const userAgent = request.get('user-agent') || ''; 12 | const tenantId = request.get('x-tenant-id') || ''; 13 | 14 | response.on('close', () => { 15 | console.log('&&&&&&&&&&&&&& ', originalUrl); 16 | const { statusCode } = response; 17 | if (statusCode < 400 && originalUrl !== '/v1/timer') { 18 | const contentLength = response.get('content-length'); 19 | this.logger.log(`[${tenantId}]: ${method} ${originalUrl} --> ${statusCode} ${contentLength} - ${userAgent} ${ip}`); 20 | } 21 | }); 22 | 23 | next(); 24 | } 25 | } -------------------------------------------------------------------------------- /src/common/providers/app-cluster.service.ts: -------------------------------------------------------------------------------- 1 | import * as cluster from 'cluster'; 2 | import * as os from 'os'; 3 | import { Injectable, Logger } from '@nestjs/common'; 4 | import { ExecuteOnlyOnce } from './execute-only-once'; 5 | 6 | const numCPUs = os.cpus().length; 7 | const Cluster: any = cluster; 8 | @Injectable() 9 | export class AppClusterService { 10 | static logger: Logger = new Logger(AppClusterService.name); 11 | constructor () { } 12 | static clusterize(callback: Function): void { 13 | 14 | if (Cluster.isMaster) { 15 | this.logger.log(`Master server started on process [${process.pid}]`); 16 | for (let i = 0; i < numCPUs; i++) { 17 | Cluster.fork(); 18 | } 19 | Cluster.on('exit', (worker, code, signal) => { 20 | this.logger.log(`Worker ${worker.process.pid} died. Restarting`); 21 | Cluster.fork(); 22 | }) 23 | 24 | let mainWorkerId = null; 25 | Cluster.on('listening', (worker, address) => { 26 | 27 | this.logger.log(`cluster listening new worker id [${worker.id}]`); 28 | if (mainWorkerId === null) { 29 | 30 | this.logger.log(`Making worker ${worker.id} to main worker`); 31 | mainWorkerId = worker.id; 32 | worker.send({ order: 'executeOnlyOnce' }); 33 | } 34 | }); 35 | } else { 36 | process.on('message', (msg) => { 37 | 38 | this.logger.log(`Worker ${process.pid} received message from master. ${msg}`); 39 | if (msg['order'] === 'executeOnlyOnce') { 40 | const executeOnlyOnce = new ExecuteOnlyOnce(); 41 | executeOnlyOnce.handleTimerEventCron( 'timerCron', 10 ); 42 | } 43 | }); 44 | this.logger.log(`Cluster server started on ${process.pid}`) 45 | callback(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/common/providers/custom-response.service.ts: -------------------------------------------------------------------------------- 1 | export default class CustomResponse { 2 | private statusCode: number 3 | private message: string 4 | public result: any 5 | constructor(statusCodes: number, message: string, result?: any) { 6 | this.statusCode = statusCodes 7 | this.message = message 8 | this.result = result 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/common/providers/execute-only-once.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { Cron, SchedulerRegistry } from '@nestjs/schedule'; 3 | import { CronJob } from 'cron'; 4 | import { HttpService } from '@nestjs/axios' 5 | import { firstValueFrom } from 'rxjs'; 6 | 7 | export class ExecuteOnlyOnce { 8 | private readonly logger = new Logger(ExecuteOnlyOnce.name); 9 | 10 | handleTimerEventCron(name, seconds) { 11 | const job = new CronJob(`0/${seconds} * * * * *`, () => { 12 | const url = `http://localhost:${process.env.TCP_PORT}${process.env.APP_BASEPATH ? `/${process.env.APP_BASEPATH}` : ''}/timer`; 13 | const httpService = new HttpService(); 14 | const axiosConfig = { 15 | url, 16 | method: 'get' 17 | }; 18 | firstValueFrom(httpService.request(axiosConfig)); 19 | 20 | }); 21 | const schedulerRegistry = new SchedulerRegistry(); 22 | schedulerRegistry.addCronJob(name, job); 23 | job.start(); 24 | 25 | this.logger.debug( 26 | `Job [${name}] has been added for every ${seconds} seconds!`, 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/common/providers/node-grpc-client.ts: -------------------------------------------------------------------------------- 1 | import * as grpc from '@grpc/grpc-js'; 2 | import * as protoLoader from '@grpc/proto-loader'; 3 | import { ServiceClientConstructor, GrpcObject, ServiceDefinition } from '@grpc/grpc-js/build/src/make-client'; 4 | 5 | interface Options { 6 | metadata?: any; 7 | keepCase?: boolean; 8 | longs?: string; 9 | enums?: string; 10 | default?: boolean; 11 | } 12 | 13 | export default class GRPCClient { 14 | private packageDefinition: any; 15 | private client: any; 16 | private listNameMethods: string[]; 17 | constructor (protoPath, packageName, _service, host, options?: Options) { 18 | 19 | this.packageDefinition = protoLoader.loadSync(protoPath); 20 | 21 | const proto = ((packageName) => { 22 | 23 | const packagePath = packageName.split('.'); 24 | let _proto = grpc.loadPackageDefinition(this.packageDefinition); 25 | 26 | for (let $i = 0; $i <= packagePath.length - 1; $i++) { 27 | 28 | _proto = _proto[packagePath[$i]] as GrpcObject; 29 | 30 | } 31 | 32 | return _proto; 33 | 34 | })(packageName); 35 | 36 | const listMethods = this.packageDefinition[`${packageName}.${_service}`]; 37 | const serviceObj = proto[_service] as ServiceClientConstructor; 38 | this.client = new serviceObj(host, grpc.credentials.createInsecure()); 39 | this.listNameMethods = []; 40 | 41 | for (const key in listMethods) { 42 | 43 | const methodName = listMethods[key].originalName; 44 | this.listNameMethods.push(methodName); 45 | 46 | this[`${methodName}Async`] = (data, fnAnswer, options?: Options) => { 47 | 48 | let metadataGrpc = {}; 49 | 50 | if (('metadata' in options) && (typeof options?.metadata == 'object')) { 51 | 52 | metadataGrpc = this.generateMetadata(options?.metadata) 53 | 54 | } 55 | this.client[methodName](data, metadataGrpc, fnAnswer); 56 | 57 | } 58 | 59 | this[`${methodName}Stream`] = (data, options?: Options) => { 60 | 61 | let metadataGrpc = {}; 62 | 63 | if (('metadata' in options) && (typeof options?.metadata == 'object')) { 64 | 65 | metadataGrpc = this.generateMetadata(options?.metadata) 66 | 67 | } 68 | 69 | return this.client[methodName](data, metadataGrpc) 70 | 71 | } 72 | 73 | this[`${methodName}Sync`] = (data, options?: Options) => { 74 | 75 | let metadataGrpc = {}; 76 | 77 | if (('metadata' in options) && (typeof options?.metadata == 'object')) { 78 | 79 | metadataGrpc = this.generateMetadata(options?.metadata) 80 | 81 | } 82 | 83 | const client = this.client; 84 | 85 | return new Promise(function (resolve, reject) { 86 | client[methodName](data, metadataGrpc, (err, dat) => { 87 | 88 | if (err) { 89 | return reject(err); 90 | } 91 | 92 | resolve(dat); 93 | 94 | }); 95 | 96 | }) 97 | 98 | } 99 | 100 | } 101 | 102 | } 103 | 104 | generateMetadata = (metadata) => { 105 | 106 | let metadataGrpc = new grpc.Metadata(); 107 | 108 | for (let [key, val] of Object.entries(metadata)) { 109 | metadataGrpc.add(key, val as any); 110 | 111 | } 112 | 113 | return metadataGrpc 114 | 115 | }; 116 | 117 | runService(fnName, data, fnAnswer, options?: Options) { 118 | 119 | let metadataGrpc = {}; 120 | 121 | if (('metadata' in options) && (typeof options?.metadata == 'object')) { 122 | 123 | metadataGrpc = this.generateMetadata(options?.metadata) 124 | 125 | } 126 | 127 | this.client[fnName](data, metadataGrpc, fnAnswer); 128 | 129 | } 130 | 131 | listMethods() { 132 | 133 | return this.listNameMethods; 134 | 135 | } 136 | 137 | } // End GRPCClient -------------------------------------------------------------------------------- /src/common/utils/date-utils.ts: -------------------------------------------------------------------------------- 1 | export class DateUtils { 2 | beginOfDayTimestamp(timestamp) { 3 | return timestamp - (timestamp % 86400); 4 | } 5 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV === 'local' ? require('dotenv').config({ path: `envs/.env.${process.env.NODE_ENV}`, override: true }) : ''; 2 | 3 | process.env.NODE_OPTIONS = "--max-old-space-size=1024"; 4 | 5 | import { NestFactory } from '@nestjs/core' 6 | import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger' 7 | import { ValidationPipe } from '@nestjs/common' 8 | import { Transport } from '@nestjs/microservices' 9 | import { join } from 'path' 10 | import { AppModule } from './app.module' 11 | import { AppClusterService } from './common/providers/app-cluster.service'; 12 | import { Logger } from '@nestjs/common'; 13 | import { json } from 'body-parser'; 14 | 15 | const { 16 | TCP_PORT = 30009, 17 | GRPC_HOST, 18 | GRPC_PORT = 40009, 19 | SWAGGER_BASEPATH = '', 20 | SWAGGER_TITLE, 21 | SWAGGER_DESC, 22 | SWAGGER_VERSION, 23 | SWAGGER_TAG = '', 24 | SWAGGER_DOC_PATH = '', 25 | APP_BASEPATH, 26 | } = process.env; 27 | 28 | declare const module: any 29 | 30 | // const microserviceOptions = { 31 | // // transport: Transport.REDIS, <-- Change this 32 | // transport: Transport.GRPC, // <-- to this 33 | // options: { 34 | // // url: 'redis://localhost:6379', <-- remove this 35 | // package: 'app', // <-- add this 36 | // protoPath: [ 37 | // join(__dirname, '../src/shared/proto/common-requests/common-requests.proto') 38 | // ], // <-- & this 39 | // host: GRPC_HOST, 40 | // port: GRPC_PORT, 41 | // }, 42 | // } 43 | 44 | 45 | 46 | async function bootstrap() { 47 | const app = await NestFactory.create(AppModule, { 48 | logger: ['error', 'warn', 'debug', 'log', 'verbose'] 49 | }) 50 | app.setGlobalPrefix(APP_BASEPATH); 51 | app.use(json({ limit: '5mb' })); 52 | app.enableCors(); 53 | 54 | // Swagger configuration 55 | const swaggerConfig = new DocumentBuilder() 56 | .addServer(SWAGGER_BASEPATH) 57 | .setTitle(SWAGGER_TITLE) 58 | .setDescription(SWAGGER_DESC) 59 | .setVersion(SWAGGER_VERSION) 60 | .addTag(SWAGGER_TAG) 61 | .addBearerAuth() 62 | .build() 63 | 64 | const document = SwaggerModule.createDocument(app, swaggerConfig) 65 | 66 | SwaggerModule.setup(SWAGGER_DOC_PATH, app, document) 67 | // enable validations 68 | app.useGlobalPipes(new ValidationPipe({ 69 | transform: true, whitelist: true, forbidNonWhitelisted: true 70 | })) 71 | // app.useGlobalFilters(new ErrorFilter()) 72 | // microservice grpc #1 73 | // app.connectMicroservice(microserviceOptions) 74 | // await app.startAllMicroservices() 75 | // await app.listen(microserviceOptions.options.port) 76 | // await app.listen(TCP_PORT) 77 | await app.init() 78 | await app.listen(TCP_PORT); 79 | Logger.log(`[main] Business Process Engine is running on: ${await app.getUrl()}`); 80 | if (module.hot) { 81 | module.hot.accept() 82 | module.hot.dispose(() => app.close()) 83 | } 84 | 85 | process.on('SIGINT', () => { 86 | app.close() 87 | Logger.warn(`[main] Server has been closed due to the signal [SIGINT]`); 88 | }); 89 | } 90 | 91 | AppClusterService.clusterize(bootstrap); 92 | 93 | // bootstrap() 94 | -------------------------------------------------------------------------------- /src/models/process-definitions/process-definitions.schema.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | import mongoose, { Document, Types, Schema as SchemaTypes } from 'mongoose'; 3 | import { StageTypes, StageSubTypes, ConnectorTypes } from 'src/common/const/enums'; 4 | 5 | 6 | export type ProcessDefinitionDocument = ProcessDefinition & Document; 7 | 8 | class CompiledDefinition { } 9 | 10 | class User { 11 | @Prop() 12 | userId: string; 13 | 14 | @Prop() 15 | emailId: string; 16 | } 17 | class Expression { 18 | @Prop() 19 | lhs: SchemaTypes.Types.Mixed; 20 | 21 | @Prop() 22 | op: string; 23 | 24 | @Prop() 25 | rhs: SchemaTypes.Types.Mixed 26 | } 27 | 28 | class Condition { 29 | @Prop() 30 | name: string; 31 | 32 | @Prop() 33 | op: string; 34 | 35 | @Prop({ type: [Expression] }) 36 | expressions: Expression[] 37 | 38 | @Prop() 39 | onTrueNextStage: string; 40 | 41 | @Prop() 42 | onFalseNextStage: string 43 | } 44 | 45 | class PropertyType { 46 | @Prop() 47 | type: string; 48 | 49 | @Prop() 50 | arrayOf: string; 51 | 52 | @Prop() 53 | default: string; 54 | 55 | @Prop() 56 | required: boolean; 57 | 58 | @Prop() 59 | enum: [any]; 60 | 61 | @Prop() 62 | properties: [any]; 63 | 64 | @Prop() 65 | displayType: string; 66 | } 67 | 68 | class Property { 69 | @Prop() 70 | key: string; 71 | 72 | @Prop() 73 | section: string; 74 | 75 | @Prop({ type: PropertyType }) 76 | value: PropertyType; 77 | } 78 | 79 | class DefinitionCriteria extends Document { 80 | @Prop() 81 | allCompleted: boolean; 82 | 83 | @Prop() 84 | anyCompleted: boolean; 85 | 86 | @Prop() 87 | allActivitiesCompleted: boolean; 88 | 89 | @Prop() 90 | anyActivitiesCompleted: boolean; 91 | 92 | @Prop() 93 | allSuccess: boolean; 94 | 95 | @Prop() 96 | anySuccess: boolean; 97 | 98 | @Prop({ type: Boolean, default: true }) 99 | mandatoryCompleted: boolean; 100 | } 101 | 102 | class StageCriteria extends Document { 103 | @Prop({ type: Boolean, default: true }) 104 | onErrorComplete: boolean; 105 | 106 | @Prop({ type: Boolean, default: false }) 107 | showError: boolean; 108 | } 109 | 110 | class Connector { 111 | @Prop({ type: String, enum: ConnectorTypes }) 112 | type: ConnectorTypes; 113 | 114 | @Prop() 115 | config: SchemaTypes.Types.Mixed 116 | } 117 | 118 | @Schema() 119 | class StageDefinitionSchema { 120 | @Prop() 121 | key: string; 122 | 123 | @Prop() 124 | name: string; 125 | 126 | @Prop() 127 | displayName: string; 128 | 129 | @Prop() 130 | description: string; 131 | 132 | @Prop({ type: String, enum: StageTypes, default: 'activity' }) 133 | type: StageTypes; //start, end, gateway, system task, timer 134 | 135 | @Prop({ type: String, enum: StageSubTypes, default: 'system-task' }) 136 | subType: StageSubTypes; //start, end, gateway, system task, timer 137 | 138 | @Prop({ default: true }) 139 | auto: boolean; // execute stage automatically or trigger manually 140 | 141 | @Prop({ default: false }) 142 | disabled: boolean; // disable stage 143 | 144 | @Prop({ default: true }) 145 | mandatory: boolean; // disable stage 146 | 147 | @Prop({ type: [String] }) 148 | nextStages: string[]; 149 | 150 | @Prop([Property]) 151 | properties: Property[]; 152 | 153 | @Prop([Condition]) 154 | conditions: Condition[]; 155 | 156 | @Prop([String]) 157 | parallelStages: string[]; 158 | 159 | 160 | @Prop({ type: String }) 161 | assignee: string; 162 | 163 | @Prop({ type: [String] }) 164 | watchers: string[]; 165 | 166 | @Prop({ type: StageCriteria }) 167 | criteria?: StageCriteria; 168 | 169 | 170 | @Prop({ type: Connector, default: null }) 171 | connector?: Connector; 172 | 173 | @Prop() 174 | processDefinitionId?: string; 175 | 176 | @Prop() 177 | processDefinitionKey?: string; 178 | 179 | @Prop({ default: 0 }) 180 | estimatedTimeDuration?: number; // milliseconds 181 | 182 | @Prop() 183 | priority?: string; 184 | 185 | } 186 | 187 | 188 | @Schema({ timestamps: true }) 189 | export class ProcessDefinition extends Document { 190 | 191 | @Prop({ required: true }) 192 | name: string; 193 | 194 | @Prop({ index: true }) 195 | key: string; 196 | 197 | @Prop({ default: false }) 198 | isParallel: boolean; 199 | 200 | @Prop({ type: DefinitionCriteria, default: null }) 201 | criteria?: DefinitionCriteria; 202 | 203 | @Prop() 204 | description: string; 205 | 206 | @Prop({ required: true }) 207 | version: number; 208 | 209 | @Prop([Property]) 210 | properties: Property[]; 211 | 212 | @Prop([StageDefinitionSchema]) 213 | stages: StageDefinitionSchema[]; 214 | 215 | @Prop({ type: Connector }) 216 | assigneeConnector?: Connector; // this field is temporary 217 | 218 | @Prop({ type: CompiledDefinition }) 219 | _compiledDefinition?: CompiledDefinition; 220 | 221 | @Prop() 222 | createdBy?: User; 223 | 224 | @Prop() 225 | updatedBy?: User; 226 | } 227 | 228 | export const ProcessDefinitionSchema = SchemaFactory.createForClass(ProcessDefinition); 229 | ProcessDefinitionSchema.index({ key: 1, version: 1 }, { unique: true }); -------------------------------------------------------------------------------- /src/models/process-definitions/repository/process-definitions.repository.d.ts: -------------------------------------------------------------------------------- 1 | import { UpdateResult } from 'typeorm'; 2 | import { BaseRepositry } from 'src/base/base.repository'; 3 | import { CreateProcessDefinitionDto, UpdateProcessDefinitionDto } from '../../../modules/process-definitions/dtos'; 4 | import { ProcessDefinitionDocument } from 'src/models/process-definitions/process-definitions.schema'; 5 | 6 | @Injectable() 7 | export interface ProcessDefinitionRepository extends BaseRepositry { 8 | create(setting: CreateProcessDefinitionDto): Promise; 9 | update(condition: any, user: UpdateProcessDefinitionDto): Promise; 10 | } -------------------------------------------------------------------------------- /src/models/process-definitions/repository/process-definitions.repository.impl.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Scope } from '@nestjs/common'; 2 | import { Model } from 'mongoose'; 3 | import { ProcessDefinition, ProcessDefinitionDocument } from 'src/models/process-definitions/process-definitions.schema'; 4 | import { BaseRepositoryImpl } from '../../../shared/repositories/base/base-repository.impl'; 5 | import { CreateProcessDefinitionDto, UpdateProcessDefinitionDto } from 'src/modules/process-definitions/dtos'; 6 | import { ProcessDefinitionRepository } from './process-definitions.repository'; 7 | import { UpdateStageBody } from 'src/modules/process-definitions/dtos/update-stage.dto'; 8 | import { InjectModel } from '@nestjs/mongoose'; 9 | 10 | @Injectable() 11 | export class ProcessDefinitionRepositoryImpl extends BaseRepositoryImpl implements ProcessDefinitionRepository { 12 | constructor (@InjectModel(ProcessDefinition.name) protected ProcessDefinitionModel: Model) { 13 | super(ProcessDefinitionModel); 14 | } 15 | 16 | create(object: CreateProcessDefinitionDto): Promise { 17 | const event = new this.ProcessDefinitionModel(object); 18 | return event.save(); 19 | } 20 | 21 | update(condition: any, object: UpdateProcessDefinitionDto): Promise { 22 | 23 | return this.ProcessDefinitionModel.findOneAndUpdate(condition, object, { new: true }).exec(); 24 | } 25 | 26 | updateStage(condition: any, object: any): Promise { 27 | 28 | return this.ProcessDefinitionModel.findOneAndUpdate(condition, object, { new: true }).exec(); 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /src/models/process-instances/process-instances.schema.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | import mongoose, { Document, Types, Schema as SchemaTypes } from 'mongoose'; 3 | import { StageTypes, StageSubTypes, ConnectorTypes } from 'src/common/const/enums'; 4 | 5 | 6 | export type ProcessInstanceDocument = ProcessInstance & Document; 7 | 8 | class Dependency extends Document { 9 | @Prop() 10 | processDefinitionKey: string; 11 | 12 | @Prop() 13 | stageKey: string; 14 | } 15 | 16 | class Expression extends Document { 17 | @Prop() 18 | lhs: SchemaTypes.Types.Mixed; 19 | 20 | @Prop() 21 | op: string; 22 | 23 | @Prop() 24 | rhs: SchemaTypes.Types.Mixed 25 | 26 | @Prop() 27 | _lhs: SchemaTypes.Types.Mixed; 28 | 29 | @Prop() 30 | _rhs: SchemaTypes.Types.Mixed 31 | 32 | @Prop() 33 | _valid: boolean; 34 | } 35 | 36 | class Condition extends Document { 37 | @Prop() 38 | name: string; 39 | 40 | @Prop() 41 | op: string; 42 | 43 | @Prop({ type: [Expression] }) 44 | expressions: Expression[] 45 | 46 | @Prop() 47 | _allValid: boolean; 48 | 49 | @Prop() 50 | _anyValid: boolean; 51 | 52 | @Prop() 53 | onTrueNextStage: string; 54 | 55 | @Prop() 56 | onFalseNextStage: string 57 | } 58 | 59 | class Params extends Document { 60 | [key: string]: any; 61 | } 62 | 63 | 64 | class stageIndexJSON extends Document { 65 | } 66 | class PropertyType extends Document { 67 | @Prop() 68 | type: string; 69 | 70 | @Prop() 71 | arrayOf: string; 72 | 73 | @Prop() 74 | default: string; 75 | 76 | @Prop() 77 | required: boolean; 78 | 79 | @Prop() 80 | enum: [any]; 81 | 82 | @Prop() 83 | properties: [any]; 84 | 85 | @Prop() 86 | displayType: string; 87 | } 88 | 89 | class Property extends Document { 90 | @Prop() 91 | key: string; 92 | 93 | @Prop() 94 | section: string; 95 | 96 | @Prop({ type: PropertyType }) 97 | value: PropertyType; 98 | } 99 | 100 | class DefinitionCriteria extends Document { 101 | @Prop() 102 | allCompleted: boolean; 103 | 104 | @Prop() 105 | anyCompleted: boolean; 106 | 107 | @Prop() 108 | allActivitiesCompleted: boolean; 109 | 110 | @Prop() 111 | anyActivitiesCompleted: boolean; 112 | 113 | @Prop() 114 | allSuccess: boolean; 115 | 116 | @Prop() 117 | anySuccess: boolean; 118 | 119 | @Prop({ type: Boolean, default: true }) 120 | mandatoryCompleted: boolean; 121 | } 122 | 123 | class StageCriteria extends Document { 124 | @Prop({ type: Boolean, default: true }) 125 | onErrorComplete: boolean; 126 | 127 | @Prop({ type: Boolean, default: false }) 128 | showError: boolean; 129 | } 130 | 131 | class Flags extends Document { 132 | @Prop() 133 | _error: boolean; 134 | 135 | @Prop() 136 | _allCompleted: boolean; 137 | 138 | @Prop() 139 | _anyCompleted: boolean; 140 | 141 | @Prop() 142 | _allActivitiesCompleted: boolean; 143 | 144 | @Prop() 145 | _anyActivitiesCompleted: boolean; 146 | 147 | @Prop() 148 | _allSuccess: boolean; 149 | 150 | @Prop() 151 | _anySuccess: boolean; 152 | 153 | @Prop() 154 | _mandatoryCompleted: boolean; 155 | } 156 | 157 | class Connector { 158 | @Prop({ type: String, enum: ConnectorTypes }) 159 | type: ConnectorTypes; 160 | 161 | @Prop() 162 | config: SchemaTypes.Types.Mixed 163 | } 164 | 165 | class History { 166 | @Prop() 167 | status: string; 168 | 169 | @Prop(Params) 170 | parameters: Params; 171 | 172 | @Prop() 173 | timeActivated: number; 174 | 175 | @Prop() 176 | timeStarted: number; 177 | 178 | @Prop() 179 | timeCompleted: number; 180 | 181 | @Prop() 182 | _flags: Flags; 183 | 184 | @Prop() 185 | _error: SchemaTypes.Types.Mixed 186 | 187 | @Prop() 188 | _data: SchemaTypes.Types.Mixed 189 | 190 | } 191 | 192 | @Schema() 193 | class StageInstanceSchema { 194 | _id: Types.ObjectId 195 | 196 | @Prop() 197 | key: string; 198 | 199 | @Prop() 200 | stageId: string; 201 | 202 | @Prop() 203 | name: string; 204 | 205 | @Prop() 206 | displayName: string; 207 | 208 | @Prop() 209 | description: string; 210 | 211 | @Prop({ default: 'waiting' }) 212 | status: string; 213 | 214 | @Prop({ type: String, enum: StageTypes, default: 'activity' }) 215 | type: StageTypes; //start, end, gateway, system task, timer 216 | 217 | @Prop({ type: String, enum: StageSubTypes, default: 'system-task' }) 218 | subType: StageSubTypes; //start, end, gateway, system task, timer 219 | 220 | @Prop({ default: true }) 221 | auto: boolean; // execute stage automatically or trigger manually 222 | 223 | // @Prop({ default: false }) 224 | // disabled: boolean; // disable stage 225 | 226 | @Prop({ default: true }) 227 | mandatory: boolean; // disable stage 228 | 229 | @Prop({ type: [String] }) 230 | nextStages: string[]; 231 | 232 | @Prop([Property]) 233 | properties: Property[]; 234 | 235 | @Prop([Condition]) 236 | conditions?: Condition[]; 237 | 238 | @Prop([String]) 239 | parallelStages: string[]; 240 | 241 | @Prop(Params) 242 | parameters?: Params; 243 | 244 | @Prop({ type: String }) 245 | assignee: string; 246 | 247 | @Prop({ type: [String] }) 248 | watchers: string[]; 249 | 250 | @Prop({ type: StageCriteria }) 251 | criteria?: StageCriteria; 252 | 253 | @Prop({ type: Connector, default: null }) 254 | connector?: Connector; 255 | 256 | @Prop({ required: true, default: Date.now() }) 257 | timeActivated: number; 258 | 259 | @Prop({ required: true, default: -1 }) 260 | timeStarted: number; 261 | 262 | @Prop({ required: true, default: -1 }) 263 | timeCompleted: number; 264 | 265 | @Prop({ default: 0 }) 266 | estimatedTimeDuration?: number; // milliseconds 267 | 268 | @Prop({ default: 0 }) 269 | expToCompleteAt?: number; // milliseconds, for timer event to complete 270 | 271 | @Prop() 272 | priority?: string; 273 | 274 | @Prop({ type: Flags, default: {} }) 275 | _flags: Flags; 276 | 277 | @Prop() 278 | _error: SchemaTypes.Types.Mixed 279 | 280 | @Prop() 281 | _data: SchemaTypes.Types.Mixed 282 | 283 | @Prop() 284 | processDefinitionId?: string; 285 | 286 | @Prop() 287 | processDefinitionKey?: string; 288 | 289 | @Prop() 290 | processInstanceId?: string; 291 | 292 | @Prop([History]) 293 | history: History[] 294 | 295 | } 296 | 297 | class User { 298 | @Prop() 299 | userId: string; 300 | 301 | @Prop() 302 | emailId: string; 303 | } 304 | 305 | 306 | @Schema({ timestamps: true }) 307 | export class ProcessInstance extends Document { 308 | 309 | @Prop() 310 | processDefinitionId?: string; 311 | 312 | @Prop() 313 | processDefinitionKey?: string; 314 | 315 | @Prop() 316 | parentProcessInstanceId?: string; 317 | 318 | @Prop() 319 | rootProcessInstanceId?: string; 320 | 321 | @Prop() 322 | parentTaskId?: string; 323 | 324 | @Prop({ index: true }) 325 | name: string; 326 | 327 | @Prop({ default: false }) 328 | isParallel: boolean; 329 | 330 | @Prop({ type: DefinitionCriteria, default: null }) 331 | criteria?: DefinitionCriteria; 332 | 333 | @Prop() 334 | description: string; 335 | 336 | @Prop({ required: true }) 337 | version: number; 338 | 339 | @Prop([Property]) 340 | properties: Property[]; 341 | 342 | @Prop(Params) 343 | parameters: Params; 344 | 345 | @Prop([StageInstanceSchema]) 346 | stages: StageInstanceSchema[]; 347 | 348 | @Prop({ required: true, default: Date.now() }) 349 | timeActivated: number; 350 | 351 | @Prop({ required: true, default: -1 }) 352 | timeStarted: number; 353 | 354 | @Prop({ required: true, default: -1 }) 355 | timeCompleted: number; 356 | 357 | @Prop({ required: true, default: -1 }) 358 | timeOnhold: number; 359 | 360 | @Prop({ required: true, default: -1 }) 361 | timeCancelled: number; 362 | 363 | @Prop({ required: true, default: -1 }) 364 | timeResumed: number; 365 | 366 | @Prop({ required: true, default: 'active' }) 367 | status: string; 368 | 369 | @Prop(stageIndexJSON) 370 | _stageIndexJSON: stageIndexJSON 371 | 372 | @Prop({ type: Flags, default: {} }) 373 | _flags: Flags; 374 | 375 | @Prop({ default: 0 }) 376 | _startIndex: number; 377 | 378 | @Prop({ default: -1 }) 379 | _endIndex: number; 380 | 381 | @Prop({ type: Connector }) 382 | assigneeConnector?: Connector; // this field is temporary 383 | 384 | @Prop(User) 385 | createdBy: User; 386 | 387 | @Prop(User) 388 | holdBy: User; 389 | 390 | @Prop(User) 391 | cancelledBy: User; 392 | 393 | @Prop(User) 394 | resumedBy: User; 395 | } 396 | 397 | export const ProcessInstanceSchema = SchemaFactory.createForClass(ProcessInstance); -------------------------------------------------------------------------------- /src/models/process-instances/repository/process-instances.repository.d.ts: -------------------------------------------------------------------------------- 1 | import { UpdateResult } from 'typeorm'; 2 | import { BaseRepositry } from 'src/base/base.repository'; 3 | import { CreateProcessInstanceDto, UpdateProcessInstanceDto } from '../../../modules/process-instances/dtos'; 4 | import { ProcessInstanceDocument } from 'src/models/process-instances/process-instances.schema'; 5 | 6 | @Injectable() 7 | export interface ProcessInstanceRepository extends BaseRepositry { 8 | create(setting: CreateProcessInstanceDto): Promise; 9 | updateById(id: string, setValues: UpdateProcessInstanceDto): Promise; 10 | updateOne(condition: any, setValues: UpdateProcessInstanceDto): Promise; 11 | updateMany(condition: any, setValues: UpdateProcessInstanceDto): Promise; 12 | 13 | } -------------------------------------------------------------------------------- /src/models/process-instances/repository/process-instances.repository.impl.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Scope } from '@nestjs/common'; 2 | import { Model } from 'mongoose'; 3 | import { ProcessInstance, ProcessInstanceDocument } from 'src/models/process-instances/process-instances.schema'; 4 | import { BaseRepositoryImpl } from '../../../shared/repositories/base/base-repository.impl'; 5 | import { CreateProcessInstanceDto, UpdateProcessInstanceDto } from 'src/modules/process-instances/dtos'; 6 | import { ProcessInstanceRepository } from './process-instances.repository'; 7 | import { InjectModel } from '@nestjs/mongoose'; 8 | 9 | @Injectable() 10 | export class ProcessInstanceRepositoryImpl extends BaseRepositoryImpl implements ProcessInstanceRepository { 11 | constructor (@InjectModel(ProcessInstance.name) protected ProcessInstanceModel: Model) { 12 | super(ProcessInstanceModel); 13 | } 14 | 15 | create(object: CreateProcessInstanceDto): Promise { 16 | const event = new this.ProcessInstanceModel(object); 17 | return event.save(); 18 | } 19 | 20 | updateById(id: string, object: UpdateProcessInstanceDto): Promise { 21 | return this.ProcessInstanceModel.findOneAndUpdate({ _id: id }, object, { new: true }).exec(); 22 | } 23 | 24 | updateOne(condition: any, object: any): Promise { 25 | return this.ProcessInstanceModel.findOneAndUpdate(condition, object, { new: true }).exec(); 26 | } 27 | 28 | updateMany(condition: any, object: any): Promise { 29 | return this.ProcessInstanceModel.updateMany(condition, object).exec(); 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /src/models/user-tasks/repository/user-tasks.repository.d.ts: -------------------------------------------------------------------------------- 1 | import { UpdateResult } from 'typeorm'; 2 | import { BaseRepositry } from 'src/base/base.repository'; 3 | import { CreateUserTaskDto, UpdateUserTaskDto } from '../../../modules/tasks/dtos'; 4 | import { UserTasksDocument } from 'src/models/user-tasks/user-tasks.schema'; 5 | 6 | @Injectable() 7 | export interface UserTasksRepository extends BaseRepositry { 8 | create(task: CreateUserTaskDto): Promise; 9 | insertMany(tasks: CreateUserTaskDto[]): Promise; 10 | update(id: string, user: UpdateUserTaskDto): Promise; 11 | } -------------------------------------------------------------------------------- /src/models/user-tasks/repository/user-tasks.repository.impl.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Scope } from '@nestjs/common'; 2 | import { Model } from 'mongoose'; 3 | import { UserTasks, UserTasksDocument } from '../user-tasks.schema'; 4 | import { BaseRepositoryImpl } from '../../../shared/repositories/base/base-repository.impl'; 5 | import { CreateUserTaskDto, UpdateUserTaskDto } from 'src/modules/tasks/dtos'; 6 | import { UserTasksRepository } from './user-tasks.repository'; 7 | import { InjectModel } from '@nestjs/mongoose'; 8 | 9 | @Injectable() 10 | export class UserTasksRepositoryImpl extends BaseRepositoryImpl implements UserTasksRepository { 11 | constructor (@InjectModel(UserTasks.name) protected UserTaskModel: Model) { 12 | super(UserTaskModel); 13 | } 14 | 15 | create(object: CreateUserTaskDto): Promise { 16 | const event = new this.UserTaskModel(object); 17 | return event.save(); 18 | } 19 | 20 | update(id: string, object: UpdateUserTaskDto): Promise { 21 | return this.UserTaskModel.findOneAndUpdate({ _id: id }, object, { new: true }).exec(); 22 | } 23 | 24 | insertMany(array: CreateUserTaskDto[]): Promise { 25 | return this.UserTaskModel.insertMany(array); 26 | } 27 | 28 | updateOne(condition: any, object: any): Promise { 29 | return this.UserTaskModel.findOneAndUpdate(condition, object, { new: true }).exec(); 30 | } 31 | } -------------------------------------------------------------------------------- /src/models/user-tasks/user-tasks.schema.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | import { Document, Schema as SchemaTypes } from 'mongoose'; 3 | 4 | export type UserTasksDocument = UserTasks & Document; 5 | 6 | class PropertyType { 7 | @Prop() 8 | type: string; 9 | 10 | @Prop() 11 | arrayOf: string; 12 | 13 | @Prop() 14 | default: string; 15 | 16 | @Prop() 17 | required: boolean; 18 | 19 | @Prop() 20 | enum: [any]; 21 | 22 | @Prop() 23 | properties: [any]; 24 | 25 | @Prop() 26 | displayType: string; 27 | } 28 | 29 | class Property { 30 | @Prop() 31 | key: string; 32 | 33 | @Prop() 34 | section: string; 35 | 36 | @Prop({ type: PropertyType }) 37 | value: PropertyType; 38 | } 39 | 40 | class User { 41 | @Prop() 42 | userId: string; 43 | 44 | @Prop() 45 | emailId: string; 46 | } 47 | 48 | class Params extends Document { 49 | [key: string]: any; 50 | } 51 | 52 | @Schema({ timestamps: true }) 53 | export class UserTasks extends Document { 54 | 55 | @Prop() 56 | processDefinitionId?: string; 57 | 58 | @Prop() 59 | processDefinitionKey?: string; 60 | 61 | @Prop({ required: true }) 62 | processInstanceId: string; 63 | 64 | @Prop({ required: true }) 65 | rootProcessInstanceId: string; 66 | 67 | @Prop({ required: true }) 68 | taskId: string; 69 | 70 | @Prop() 71 | key: string; 72 | 73 | @Prop({ required: true }) 74 | summary: string; 75 | 76 | @Prop() 77 | description: string; 78 | 79 | @Prop({ type: String }) 80 | assignee: string; 81 | 82 | @Prop({ type: [String] }) 83 | watchers: string[]; 84 | 85 | @Prop([Property]) 86 | properties: Property[]; 87 | 88 | @Prop({ default: -1 }) 89 | expStartDate: number; 90 | 91 | @Prop({ default: -1 }) 92 | expEndDate: number; 93 | 94 | @Prop({ default: null }) 95 | priority: string; 96 | 97 | @Prop(Params) 98 | parameters: Params; 99 | 100 | @Prop({ required: true, default: -1 }) 101 | timeStarted: number; 102 | 103 | @Prop({ required: true, default: -1 }) 104 | timeCompleted: number; 105 | 106 | @Prop({ required: true, default: 'todo' }) 107 | status: string; 108 | 109 | @Prop(User) 110 | createdBy: User; 111 | 112 | @Prop(User) 113 | completedBy: User; 114 | 115 | @Prop(User) 116 | updatedBy: User; 117 | 118 | } 119 | 120 | export const UserTasksSchema = SchemaFactory.createForClass(UserTasks); 121 | UserTasksSchema.index({ processInstanceId: 1, taskId: 1, userId: 1 }, { unique: true }); 122 | -------------------------------------------------------------------------------- /src/modules/index.ts: -------------------------------------------------------------------------------- 1 | export * from './process-definitions/process-definitions.module'; 2 | export * from './process-instances/process-instances.module' 3 | export * from './tasks/tasks.module' 4 | -------------------------------------------------------------------------------- /src/modules/process-definitions/dtos/create-process-definition.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { 3 | IsString, 4 | IsNotEmpty, 5 | IsNotEmptyObject, 6 | IsInt, 7 | IsBoolean, 8 | ValidateNested, 9 | IsDate, 10 | IsDateString, 11 | IsEmail, 12 | IsObject, 13 | IsArray, 14 | IsNumber, 15 | IsOptional, 16 | IsIn, 17 | IsEnum, 18 | ArrayUnique, 19 | Length, 20 | Max 21 | } from 'class-validator'; 22 | import { Type } from 'class-transformer'; 23 | import { StageTypes, StageSubTypes, ConnectorTypes } from 'src/common/const/enums'; 24 | 25 | class User { 26 | @ApiProperty() 27 | @IsOptional() 28 | userId: string; 29 | 30 | @ApiProperty() 31 | @IsOptional() 32 | emailId: string; 33 | } 34 | class Expression { 35 | @ApiProperty() 36 | lhs: any; 37 | 38 | @ApiProperty() 39 | op: string; 40 | 41 | @ApiProperty() 42 | rhs: any; 43 | } 44 | 45 | class Condition { 46 | @ApiProperty() 47 | name: string; 48 | 49 | @ApiProperty() 50 | op: string; 51 | 52 | @ApiProperty({ type: [Expression] }) 53 | expressions: Expression[] 54 | 55 | @ApiProperty() 56 | onTrueNextStage: string; 57 | 58 | @ApiProperty() 59 | onFalseNextStage: string 60 | } 61 | 62 | class PropertyType { 63 | @ApiProperty({ type: String }) 64 | type: string; 65 | 66 | @ApiProperty({ type: String }) 67 | arrayOf: string; 68 | 69 | @IsArray() 70 | @ApiProperty({ type: [] }) 71 | @IsOptional() 72 | properties: any[]; 73 | 74 | @ApiProperty({ type: String }) 75 | default: string; 76 | 77 | @ApiProperty({ type: Boolean }) 78 | required: boolean; 79 | 80 | @ApiProperty() 81 | enum: [any]; 82 | 83 | @ApiProperty() 84 | displayType: string; 85 | } 86 | 87 | class Property { 88 | @ApiProperty() 89 | key: string; 90 | 91 | @ApiProperty() 92 | section: string; 93 | 94 | @ApiProperty({ type: PropertyType }) 95 | value: PropertyType 96 | } 97 | 98 | class DefinitionCriteria { 99 | @ApiProperty({ default: true }) 100 | @IsOptional() 101 | allCompleted: boolean; 102 | 103 | @ApiProperty({ default: false }) 104 | @IsOptional() 105 | anyCompleted: boolean; 106 | 107 | @ApiProperty({ default: true }) 108 | @IsOptional() 109 | allActivitiesCompleted: boolean; 110 | 111 | @ApiProperty({ default: false }) 112 | @IsOptional() 113 | anyActivitiesCompleted: boolean; 114 | 115 | @ApiProperty({ default: false }) 116 | @IsOptional() 117 | allSuccess: boolean; 118 | 119 | @ApiProperty({ default: false }) 120 | @IsOptional() 121 | anySuccess: boolean; 122 | 123 | @ApiProperty({ default: true }) 124 | @IsOptional() 125 | mandatoryCompleted: boolean; 126 | } 127 | 128 | class StageCriteria { 129 | @ApiProperty({ default: true }) 130 | @IsOptional() 131 | onErrorComplete: boolean; 132 | 133 | @ApiProperty({ default: false }) 134 | @IsOptional() 135 | showError: boolean; 136 | } 137 | class Config { 138 | 139 | } 140 | class Connector { 141 | @ApiProperty({ type: String }) 142 | @IsEnum(ConnectorTypes) 143 | type: ConnectorTypes; 144 | 145 | @ApiProperty() 146 | config: Config 147 | } 148 | 149 | class StageDefinition { 150 | @ApiProperty({ type: String, required: true }) 151 | key: string; 152 | 153 | @ApiProperty({ type: String }) 154 | name: string; 155 | 156 | @IsOptional() 157 | @ApiProperty({ type: String }) 158 | displayName: string; 159 | 160 | @IsOptional() 161 | @ApiProperty({ type: String }) 162 | description: string; 163 | 164 | @ApiProperty({ type: String, default: 'event' }) 165 | @IsEnum(StageTypes) 166 | @IsString() 167 | type: StageTypes; 168 | 169 | @ApiProperty({ type: String, default: 'start' }) 170 | @IsEnum(StageSubTypes) 171 | @IsString() 172 | subType: StageSubTypes; 173 | 174 | @ApiProperty({ type: Boolean, default: true }) 175 | @IsOptional() 176 | auto: boolean; // execute stage automatically or trigger manually 177 | 178 | @ApiProperty({ type: Boolean, default: false }) 179 | @IsOptional() 180 | disabled: boolean; // disable stage 181 | 182 | @ApiProperty({ type: [String], default: [] }) 183 | nextStages: string[]; 184 | 185 | @IsArray() 186 | @ApiProperty({ type: [Property] }) 187 | @IsOptional() 188 | properties: Property[]; 189 | 190 | @IsArray() 191 | @ApiProperty({ type: [Condition] }) 192 | @IsOptional() 193 | conditions: Condition[]; 194 | 195 | @ApiProperty() 196 | @IsOptional() 197 | assignee: string; 198 | 199 | @ApiProperty({ type: [String] }) 200 | @IsOptional() 201 | watchers: string[]; 202 | 203 | @ApiProperty({ type: StageCriteria }) 204 | @IsOptional() 205 | criteria?: StageCriteria; 206 | 207 | @ApiProperty({ type: Connector }) 208 | @IsOptional() 209 | connector?: Connector; 210 | 211 | @ApiProperty({ type: String }) 212 | @IsOptional() 213 | @Length(24, 24) 214 | processDefinitionId?: string; 215 | 216 | @ApiProperty({ type: String }) 217 | @IsOptional() 218 | processDefinitionKey?: string; 219 | 220 | @ApiProperty({ type: Number }) 221 | @IsOptional() 222 | estimatedTimeDuration?: number; 223 | 224 | @ApiProperty({ type: String }) 225 | @IsOptional() 226 | priority?: string; 227 | 228 | } 229 | 230 | 231 | export class CreateProcessDefinitionDto { 232 | @IsNotEmpty() 233 | @ApiProperty({ type: String, required: true }) 234 | name: string; 235 | 236 | @IsNotEmpty() 237 | @ApiProperty({ type: String, required: true }) 238 | key: string; 239 | 240 | @IsOptional() 241 | @ApiProperty({ type: String }) 242 | description: string; 243 | 244 | @IsOptional() 245 | @ApiProperty({ type: Boolean, default: false }) 246 | isParallel: boolean; 247 | 248 | @IsArray() 249 | @ApiProperty({ type: [Property] }) 250 | // @ValidateType(() => Property) 251 | // @ValidateNested({ each: true }) 252 | @Type(() => Property) 253 | properties: Property[]; 254 | 255 | @ApiProperty({ type: DefinitionCriteria }) 256 | @IsOptional() 257 | criteria?: DefinitionCriteria; 258 | 259 | @IsArray() 260 | @ApiProperty({ type: [StageDefinition], default: [] }) 261 | // @ValidateType(() => StageDefinition) 262 | // @ValidateNested({ each: true }) 263 | @Type(() => StageDefinition) 264 | stages: StageDefinition[]; 265 | 266 | @ApiProperty({ type: Connector }) 267 | @IsOptional() 268 | assigneeConnector?: Connector; // this field is temporary 269 | 270 | @ApiProperty({ type: User }) 271 | @IsOptional() 272 | createdBy?: User; // this field is temporary 273 | } 274 | 275 | function ValidateType(arg0: () => typeof StageDefinition) { 276 | throw new Error('Function not implemented.'); 277 | } 278 | -------------------------------------------------------------------------------- /src/modules/process-definitions/dtos/get-process-definitions.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsOptional, IsNumber, min, Min } from 'class-validator'; 2 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; 3 | import { Type } from 'class-transformer'; 4 | 5 | export class GetProcessDefinitionDto { 6 | @ApiProperty({ 7 | default: 0, 8 | }) 9 | @IsNumber() 10 | @Type(() => Number) 11 | readonly page: number; 12 | 13 | @ApiProperty({ 14 | default: 10, 15 | }) 16 | @IsNumber() 17 | @Type(() => Number) 18 | readonly size: number; 19 | 20 | @IsString() 21 | @ApiPropertyOptional() 22 | @IsOptional() 23 | readonly filters: string; 24 | 25 | @IsString() 26 | @ApiPropertyOptional() 27 | @IsOptional() 28 | readonly customFilters: string; 29 | 30 | @IsString() 31 | @ApiPropertyOptional() 32 | @IsOptional() 33 | readonly sort: string; 34 | 35 | @IsString() 36 | @ApiPropertyOptional() 37 | @IsOptional() 38 | readonly search: string; 39 | } 40 | 41 | 42 | export class GetProcessDefinitionQueryDto { 43 | @ApiPropertyOptional() 44 | @IsNumber() 45 | @IsOptional() 46 | @Min(1) 47 | @Type(() => Number) 48 | readonly version: number; 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/modules/process-definitions/dtos/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-process-definition.dto' 2 | export * from './get-process-definitions.dto' 3 | export * from './update-process-definition.dto' 4 | -------------------------------------------------------------------------------- /src/modules/process-definitions/dtos/update-process-definition.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; 2 | import { 3 | IsString, 4 | IsNotEmpty, 5 | IsInt, 6 | IsBoolean, 7 | ValidateNested, 8 | IsDate, 9 | IsDateString, 10 | IsEmail, 11 | IsObject, 12 | IsArray, 13 | IsNumber, 14 | IsOptional, 15 | IsIn, 16 | IsEnum, 17 | Length, 18 | } from 'class-validator' 19 | 20 | import { Type } from 'class-transformer'; 21 | import { StageTypes, StageSubTypes, ConnectorTypes } from 'src/common/const/enums'; 22 | 23 | class User { 24 | @ApiProperty() 25 | @IsOptional() 26 | userId: string; 27 | 28 | @ApiProperty() 29 | @IsOptional() 30 | emailId: string; 31 | } 32 | class PropertyType { 33 | @ApiProperty({ type: String }) 34 | type: string; 35 | 36 | @ApiProperty({ type: String }) 37 | default: string; 38 | 39 | @ApiProperty({ type: Boolean }) 40 | required: boolean; 41 | 42 | @ApiProperty() 43 | enum: [any]; 44 | } 45 | 46 | class Property { 47 | @ApiProperty() 48 | key: string; 49 | 50 | @ApiProperty({ type: PropertyType }) 51 | value: PropertyType 52 | } 53 | 54 | class DefinitionCriteria { 55 | @ApiProperty({ default: true }) 56 | @IsOptional() 57 | allCompleted: boolean; 58 | 59 | @ApiProperty({ default: false }) 60 | @IsOptional() 61 | anyCompleted: boolean; 62 | 63 | @ApiProperty({ default: true }) 64 | @IsOptional() 65 | allActivitiesCompleted: boolean; 66 | 67 | @ApiProperty({ default: false }) 68 | @IsOptional() 69 | anyActivitiesCompleted: boolean; 70 | 71 | @ApiProperty({ default: false }) 72 | @IsOptional() 73 | allSuccess: boolean; 74 | 75 | @ApiProperty({ default: false }) 76 | @IsOptional() 77 | anySuccess: boolean; 78 | 79 | @ApiProperty({ default: true }) 80 | @IsOptional() 81 | mandatoryCompleted: boolean; 82 | } 83 | class Config { 84 | 85 | } 86 | class Connector { 87 | @ApiProperty({ type: String }) 88 | @IsEnum(ConnectorTypes) 89 | type: ConnectorTypes; 90 | 91 | @ApiProperty() 92 | config: Config 93 | } 94 | 95 | class Expression { 96 | @ApiProperty() 97 | lhs: any; 98 | 99 | @ApiProperty() 100 | op: string; 101 | 102 | @ApiProperty() 103 | rhs: any; 104 | } 105 | 106 | class Condition { 107 | @ApiProperty() 108 | name: string; 109 | 110 | @ApiProperty() 111 | op: string; 112 | 113 | @ApiProperty({ type: [Expression] }) 114 | expressions: Expression[] 115 | 116 | @ApiProperty() 117 | onTrueNextStage: string; 118 | 119 | @ApiProperty() 120 | onFalseNextStage: string 121 | } 122 | 123 | 124 | export class StageDefinition { 125 | @Length(1, 24) 126 | @ApiProperty({ type: String }) 127 | key: string; 128 | 129 | @ApiProperty({ type: String }) 130 | name: string; 131 | 132 | @IsOptional() 133 | @ApiProperty({ type: String }) 134 | displayName: string; 135 | 136 | @IsOptional() 137 | @ApiProperty({ type: String }) 138 | description: string; 139 | 140 | @ApiProperty({ type: String, default: 'activity' }) 141 | @IsEnum(StageTypes) 142 | @IsString() 143 | type: StageTypes; 144 | 145 | @ApiProperty({ type: String, default: 'manual-task' }) 146 | @IsEnum(StageSubTypes) 147 | @IsString() 148 | subType: StageSubTypes; 149 | 150 | @ApiProperty({ type: Boolean, default: true }) 151 | @IsOptional() 152 | auto: boolean; // execute stage automatically or trigger manually 153 | 154 | @ApiProperty({ type: Boolean, default: false }) 155 | @IsOptional() 156 | disabled: boolean; // disable stage 157 | 158 | @ApiProperty({ type: [String], default: [] }) 159 | nextStages: string[]; 160 | 161 | @IsArray() 162 | @ApiProperty({ type: [Property] }) 163 | @IsOptional() 164 | properties: Property[]; 165 | 166 | @IsArray() 167 | @ApiProperty({ type: [Condition] }) 168 | @IsOptional() 169 | conditions: Condition[]; 170 | 171 | @ApiProperty() 172 | @IsOptional() 173 | assignee: string; 174 | 175 | @ApiProperty({ type: [String] }) 176 | @IsOptional() 177 | watchers: string[]; 178 | 179 | @ApiProperty({ type: DefinitionCriteria }) 180 | @IsOptional() 181 | criteria?: DefinitionCriteria; 182 | 183 | @ApiProperty({ type: Connector }) 184 | @IsOptional() 185 | connector: Connector; 186 | 187 | @ApiProperty({ type: String }) 188 | @IsOptional() 189 | @Length(24, 24) 190 | processDefinitionId?: string; 191 | 192 | @ApiProperty({ type: String }) 193 | @IsOptional() 194 | @Length(1, 24) 195 | processDefinitionKey?: string; 196 | } 197 | 198 | 199 | export class UpdateProcessDefinitionDto { 200 | @IsOptional() 201 | @ApiProperty() 202 | description?: string; 203 | 204 | @IsArray() 205 | @ApiProperty({ type: [Property] }) 206 | @IsOptional() 207 | properties?: Property[]; 208 | 209 | @ApiProperty({ type: DefinitionCriteria }) 210 | @IsOptional() 211 | criteria?: DefinitionCriteria; 212 | 213 | @IsArray() 214 | @ApiProperty({ type: [StageDefinition] }) 215 | // @ValidateType(() => StageDefinition) 216 | // @ValidateNested({ each: true }) 217 | @Type(() => StageDefinition) 218 | stages?: StageDefinition[]; 219 | 220 | @ApiProperty({ type: Connector }) 221 | @IsOptional() 222 | assigneeConnector?: Connector; // this field is temporary 223 | 224 | _compiledDefinition?: any; 225 | 226 | @ApiProperty({ type: User }) 227 | @IsOptional() 228 | createdBy?: User; // this field is temporary 229 | 230 | } 231 | 232 | export class UpdateDefinitionParams { 233 | @ApiPropertyOptional() 234 | @IsString() 235 | processDefinitionId: string; 236 | 237 | @ApiPropertyOptional() 238 | @IsString() 239 | stageId: string; 240 | } -------------------------------------------------------------------------------- /src/modules/process-definitions/dtos/update-stage.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; 2 | import { 3 | IsArray, 4 | IsEnum, 5 | IsOptional, 6 | IsString, 7 | Length 8 | } from 'class-validator' 9 | import { StageTypes, StageSubTypes, ConnectorTypes } from 'src/common/const/enums'; 10 | 11 | 12 | export class UpdateStageParams { 13 | @ApiPropertyOptional() 14 | @IsString() 15 | @Length(24, 24) 16 | processDefinitionId: string; 17 | 18 | @ApiPropertyOptional() 19 | @IsString() 20 | @Length(24, 24) 21 | stageId: string; 22 | } 23 | 24 | class Config { 25 | 26 | } 27 | class Connector { 28 | @ApiProperty({ type: String }) 29 | @IsEnum(ConnectorTypes) 30 | type: ConnectorTypes; 31 | 32 | @ApiProperty() 33 | config: Config 34 | } 35 | 36 | class PropertyType { 37 | @ApiProperty({ type: String }) 38 | type: string; 39 | 40 | @ApiProperty({ type: String }) 41 | default: string; 42 | 43 | @ApiProperty({ type: Boolean }) 44 | required: boolean; 45 | 46 | @ApiProperty() 47 | enum: [any]; 48 | } 49 | 50 | class Property { 51 | @ApiProperty() 52 | key: string; 53 | 54 | @ApiProperty({ type: PropertyType }) 55 | value: PropertyType 56 | } 57 | 58 | class StageCriteria { 59 | @ApiProperty({ default: true }) 60 | @IsOptional() 61 | onErrorComplete: boolean; 62 | 63 | @ApiProperty({ default: false }) 64 | @IsOptional() 65 | showError: boolean; 66 | } 67 | 68 | class Expression { 69 | @ApiProperty() 70 | lhs: any; 71 | 72 | @ApiProperty() 73 | op: string; 74 | 75 | @ApiProperty() 76 | rhs: any; 77 | } 78 | 79 | class Condition { 80 | @ApiProperty() 81 | name: string; 82 | 83 | @ApiProperty() 84 | op: string; 85 | 86 | @ApiProperty({ type: [Expression] }) 87 | expressions: Expression[] 88 | 89 | @ApiProperty() 90 | onTrueNextStage: string; 91 | 92 | @ApiProperty() 93 | onFalseNextStage: string 94 | } 95 | 96 | 97 | export class UpdateStageBody { 98 | @ApiProperty({ type: String }) 99 | @IsOptional() 100 | key?: string; 101 | 102 | @ApiProperty({ type: String }) 103 | @IsOptional() 104 | name?: string; 105 | 106 | @IsOptional() 107 | @ApiProperty({ type: String }) 108 | description?: string; 109 | 110 | @ApiProperty({ type: String, default: 'activity' }) 111 | @IsEnum(StageTypes) 112 | @IsString() 113 | @IsOptional() 114 | type?: StageTypes; 115 | 116 | @ApiProperty({ type: String, default: 'system-task' }) 117 | @IsEnum(StageSubTypes) 118 | @IsString() 119 | @IsOptional() 120 | subType?: StageSubTypes; 121 | 122 | @ApiProperty({ type: Boolean, default: true }) 123 | @IsOptional() 124 | auto?: boolean; // execute stage automatically or trigger manually 125 | 126 | @ApiProperty({ type: Boolean, default: false }) 127 | @IsOptional() 128 | disabled?: boolean; // disable stage 129 | 130 | @ApiProperty({ type: [String], default: [] }) 131 | nextStages: string[]; 132 | 133 | @IsArray() 134 | @ApiProperty({ type: [Property] }) 135 | @IsOptional() 136 | properties?: Property[]; 137 | 138 | @IsArray() 139 | @ApiProperty({ type: [Condition] }) 140 | @IsOptional() 141 | conditions?: Condition[]; 142 | 143 | @ApiProperty({ type: String }) 144 | @IsOptional() 145 | @IsOptional() 146 | assignee?: string; 147 | 148 | @ApiProperty({ type: StageCriteria }) 149 | @IsOptional() 150 | criteria?: StageCriteria; 151 | 152 | @ApiProperty({ type: Connector }) 153 | @IsOptional() 154 | connector?: Connector; 155 | 156 | @ApiProperty({ type: String }) 157 | @IsOptional() 158 | @Length(24, 24) 159 | processDefinitionId?: string; 160 | } 161 | -------------------------------------------------------------------------------- /src/modules/process-definitions/joi-validations/create-process-definition.joi.ts: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | const connectorOpenAISchema = Joi.object({ 4 | type: Joi.string().required().valid('openai'), 5 | config: Joi.object({ 6 | prompt: Joi.string().required(), 7 | apiKey: Joi.string().required(), 8 | data: Joi.object().optional(), 9 | }).required() 10 | }); 11 | 12 | const connectorRestSchema = Joi.object({ 13 | type: Joi.string().required().valid('rest'), 14 | config: Joi.object({ 15 | url: Joi.string().required(), 16 | timeout: Joi.number().default(5000), 17 | method: Joi.string().required(), 18 | headers: Joi.object().optional(), 19 | params: Joi.object().optional(), 20 | query: Joi.object().optional(), 21 | data: Joi.object().optional(), 22 | }).required() 23 | }) 24 | 25 | 26 | const connectorGrpcSchema = Joi.object({ 27 | type: Joi.string().required().valid('grpc'), 28 | config:Joi.object({ 29 | serviceOptions: Joi.object({ 30 | protoPath: Joi.string().required(), 31 | serviceName: Joi.string().required(), 32 | packageName: Joi.string().required(), 33 | url: Joi.string().required(), 34 | }).required(), 35 | methodOptions: Joi.object({ 36 | methodName: Joi.string().required(), 37 | message: Joi.object().optional(), 38 | metadata: Joi.object().optional(), 39 | }).required(), 40 | }).required() 41 | }) 42 | 43 | 44 | const expressionSchema = Joi.object({ 45 | lhs: Joi.string().required(), 46 | op: Joi.string().valid('==', '!=', '>', '<', '>=', '<=', '===').required(), 47 | rhs: Joi.any().required(), 48 | }); 49 | 50 | const conditionSchema = Joi.object({ 51 | name: Joi.string().required(), 52 | op: Joi.string().valid('and', 'or').insensitive().required(), 53 | expressions: Joi.array().items(expressionSchema).required(), 54 | onTrueNextStage: Joi.string().optional(), 55 | onFalseNextStage: Joi.string().optional(), 56 | }); 57 | 58 | 59 | const criteriaSchema = Joi.object({ 60 | allCompleted: Joi.boolean().optional(), 61 | anyCompleted: Joi.boolean().optional(), 62 | allActivitiesCompleted: Joi.boolean().optional(), 63 | anyActivitiesCompleted: Joi.boolean().optional(), 64 | allSuccess: Joi.boolean().optional(), 65 | anySuccess: Joi.boolean().optional(), 66 | onErrorComplete: Joi.boolean().optional(), 67 | showError: Joi.boolean().optional(), 68 | }); 69 | 70 | export const StagesSchema = { 71 | 72 | "start": Joi.object({ 73 | key: Joi.string().required(), 74 | name: Joi.string().required(), 75 | displayName: Joi.string().optional(), 76 | description: Joi.string().optional().allow(null, ''), 77 | type: Joi.string().valid('event').required(), 78 | subType: Joi.string().valid('start').required(), 79 | nextStages: Joi.array().items(Joi.string()).length(1).required() 80 | }), 81 | 82 | "end": Joi.object({ 83 | key: Joi.string().required(), 84 | name: Joi.string().required(), 85 | displayName: Joi.string().optional(), 86 | description: Joi.string().optional().allow(null, ''), 87 | type: Joi.string().valid('event').required(), 88 | subType: Joi.string().valid('end').required() 89 | }), 90 | 91 | 92 | 93 | "user-task": Joi.object({ 94 | key: Joi.string().required(), 95 | name: Joi.string().required(), 96 | displayName: Joi.string().optional(), 97 | description: Joi.string().optional().allow(null, ''), 98 | type: Joi.string().valid('activity').required(), 99 | mandatory: Joi.boolean().required(), 100 | subType: Joi.string().valid('user-task').required(), 101 | nextStages: Joi.array().items(Joi.string()).required(), 102 | estimatedTimeDuration: Joi.number().integer(), 103 | properties: Joi.array().items( 104 | Joi.object({ 105 | key: Joi.string().required(), 106 | value: Joi.object({ 107 | type: Joi.string().required(), 108 | default: Joi.any().required(), 109 | required: Joi.boolean().required(), 110 | enum: Joi.array().items(Joi.any()).optional(), 111 | }).required(), 112 | }).optional() 113 | ), 114 | assignee: Joi.string().allow('').optional(), 115 | criteria: criteriaSchema.allow(null).optional(), 116 | connector:Joi.alternatives().try( 117 | connectorRestSchema, 118 | connectorGrpcSchema, 119 | connectorOpenAISchema, 120 | ).allow(null).optional(), 121 | 122 | formId: Joi.string().optional(), 123 | customParams: Joi.object().optional() 124 | }), 125 | 126 | "system-task": Joi.object({ 127 | key: Joi.string().required(), 128 | name: Joi.string().required(), 129 | displayName: Joi.string().optional(), 130 | description: Joi.string().optional().allow(null, ''), 131 | type: Joi.string().valid('activity').required(), 132 | subType: Joi.string().valid('system-task').required(), 133 | auto: Joi.boolean().default(true), 134 | mandatory: Joi.boolean().required().default(true), 135 | nextStages: Joi.array().items(Joi.string()).required(), 136 | properties: Joi.array().items( 137 | Joi.object({ 138 | key: Joi.string().required(), 139 | value: Joi.object({ 140 | type: Joi.string().required(), 141 | default: Joi.any().required(), 142 | required: Joi.boolean().required(), 143 | enum: Joi.array().items(Joi.any()).optional(), 144 | }).required(), 145 | }).optional() 146 | ), 147 | criteria: criteriaSchema.allow(null).optional(), 148 | connector: Joi.alternatives().try( 149 | connectorRestSchema, 150 | connectorGrpcSchema, 151 | connectorOpenAISchema, 152 | ).allow(null).optional(), 153 | customParams: Joi.object().optional() 154 | }), 155 | 156 | "compound-task": Joi.object({ 157 | key: Joi.string().required(), 158 | name: Joi.string().required(), 159 | displayName: Joi.string().optional(), 160 | description: Joi.string().optional().allow(null, ''), 161 | type: Joi.string().valid('activity').required(), 162 | subType: Joi.string().valid('compound-task').required(), 163 | mandatory: Joi.boolean().required().default(true), 164 | nextStages: Joi.array().items(Joi.string()).required(), 165 | properties: Joi.array().items( 166 | Joi.object({ 167 | key: Joi.string().required(), 168 | value: Joi.object({ 169 | type: Joi.string().required(), 170 | default: Joi.any().required(), 171 | required: Joi.boolean().required(), 172 | enum: Joi.array().items(Joi.any()).optional(), 173 | }).required(), 174 | }).optional() 175 | ), 176 | criteria: criteriaSchema.allow(null).optional(), 177 | processDefinitionKey: Joi.string().allow(null, ''), //either of the processDefinitionKey , processDefinitionId is required 178 | processDefinitionId: Joi.string().length(24).allow(null, '') 179 | }).xor('processDefinitionKey', 'processDefinitionId'), 180 | 181 | "exclusive": Joi.object({ 182 | key: Joi.string().required(), 183 | name: Joi.string().required(), 184 | displayName: Joi.string().optional(), 185 | description: Joi.string().optional().allow(null, ''), 186 | type: Joi.string().valid('gateway').required(), 187 | subType: Joi.string().valid('exclusive').required(), 188 | nextStages: Joi.array().items(Joi.string()).max(1).required(), 189 | conditions: Joi.array().items(conditionSchema).required(), 190 | }), 191 | "inclusive": Joi.object({ 192 | key: Joi.string().required(), 193 | name: Joi.string().required(), 194 | displayName: Joi.string().optional(), 195 | description: Joi.string().optional().allow(null, ''), 196 | type: Joi.string().valid('gateway').required(), 197 | subType: Joi.string().valid('inclusive').required(), 198 | nextStages: Joi.array().items(Joi.string()).max(1).required(), 199 | conditions: Joi.array().items(conditionSchema).required(), 200 | }) 201 | 202 | }; 203 | 204 | 205 | -------------------------------------------------------------------------------- /src/modules/process-definitions/process-definitions.grpc.controller.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iauroSystems/io-flow-core/1db91a94901a2a5a2d51adb233f15b89bcd21c56/src/modules/process-definitions/process-definitions.grpc.controller.ts -------------------------------------------------------------------------------- /src/modules/process-definitions/process-definitions.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { MongooseModule } from "@nestjs/mongoose"; 3 | import { ProcessDefinition, ProcessDefinitionSchema } from "src/models/process-definitions/process-definitions.schema"; 4 | import { ProcessDefinitionRepositoryImpl } from "src/models/process-definitions/repository/process-definitions.repository.impl"; 5 | import { GrpcConnector, OpenAIConnector } from "src/shared/connectors"; 6 | import { Compiler } from "../process-instances/providers"; 7 | import { ProcessDefinitionController } from "./process-definitions.rest.controller"; 8 | import { ProcessDefinitionService } from "./process-definitions.service"; 9 | 10 | 11 | @Module({ 12 | imports: [MongooseModule.forFeature([{ name: ProcessDefinition.name, schema: ProcessDefinitionSchema }])], 13 | controllers: [ProcessDefinitionController], 14 | providers: [ProcessDefinitionService, ProcessDefinitionRepositoryImpl, GrpcConnector,OpenAIConnector, Compiler], 15 | }) 16 | export class ProcessDefinitionsModule { } -------------------------------------------------------------------------------- /src/modules/process-definitions/process-definitions.rest.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, HttpCode, HttpStatus, Param, Patch, Post, Put, Query, Headers, UseInterceptors, UploadedFile, UploadedFiles, UseGuards } from "@nestjs/common"; 2 | import { ApiBody, ApiConsumes, ApiHeader, ApiTags } from "@nestjs/swagger"; 3 | import CustomResponse from 'src/common/providers/custom-response.service'; 4 | import CustomError from "src/common/exceptions/custom-error"; 5 | import { ProcessDefinition, ProcessDefinitionDocument } from "src/models/process-definitions/process-definitions.schema"; 6 | import { CreateProcessDefinitionDto, UpdateProcessDefinitionDto, GetProcessDefinitionDto, UpdateDefinitionParams, StageDefinition, GetProcessDefinitionQueryDto } from "./dtos"; 7 | import { ProcessDefinitionService } from "./process-definitions.service"; 8 | import { UpdateStageBody, UpdateStageParams } from "./dtos/update-stage.dto"; 9 | import { CommonHeadersDto } from "src/shared/dtos"; 10 | import { FileInterceptor } from "@nestjs/platform-express"; 11 | import { multerOptions } from "./providers/multer-options"; 12 | 13 | @ApiTags('Business Process Definitions') 14 | @Controller('process-definitions') 15 | 16 | export class ProcessDefinitionController { 17 | constructor (private processDefinitionService: ProcessDefinitionService) { } 18 | 19 | @Post() 20 | @HttpCode(HttpStatus.CREATED) 21 | async create(@Headers() headers: CommonHeadersDto, @Body() createProcessDefinitionDto: CreateProcessDefinitionDto): Promise { 22 | return this.processDefinitionService.create(headers, createProcessDefinitionDto); 23 | } 24 | 25 | @Get('/list') 26 | async findAll(@Headers() headers: CommonHeadersDto, @Query() getProcessDefinitionDto: GetProcessDefinitionDto): Promise { 27 | return this.processDefinitionService.findAll(getProcessDefinitionDto); 28 | } 29 | 30 | @Get(':processDefinitionId') 31 | async findOne(@Headers() headers: CommonHeadersDto, @Param('processDefinitionId') id: string): Promise { 32 | return this.processDefinitionService.findOne(id); 33 | } 34 | 35 | @Get('key/:key') 36 | async findOneByKey(@Headers() headers: CommonHeadersDto, @Param('key') key: string, @Query() query: GetProcessDefinitionQueryDto): Promise { 37 | return this.processDefinitionService.findOneByKey(key, query); 38 | } 39 | 40 | @Put(':processDefinitionId') 41 | async updateOne(@Headers() headers: CommonHeadersDto, @Param('processDefinitionId') id: string, @Body() updateProcessDefinitionDto: UpdateProcessDefinitionDto): 42 | Promise { 43 | return this.processDefinitionService.update(headers, id, updateProcessDefinitionDto); 44 | } 45 | 46 | @Patch(':processDefinitionId/stages/:stageId') 47 | async updateStage(@Headers() headers: CommonHeadersDto, @Param() params: UpdateStageParams, @Body() stage: UpdateStageBody): 48 | Promise { 49 | return this.processDefinitionService.updateStage(headers, params, stage); 50 | } 51 | 52 | @Post('upload-connector') 53 | @ApiConsumes("multipart/form-data") 54 | @ApiBody({ 55 | schema: { 56 | type: "object", 57 | properties: { 58 | file: { 59 | type: "string", 60 | format: "binary", 61 | }, 62 | serviceName: { 63 | type: "string" 64 | } 65 | }, 66 | }, 67 | }) 68 | @UseInterceptors(FileInterceptor('file', multerOptions)) 69 | uploadPROTOFile( 70 | @Headers() headers: CommonHeadersDto, 71 | @UploadedFile() file: Express.Multer.File, 72 | ) { 73 | return this.processDefinitionService.uploadProtoFile(headers, file); 74 | } 75 | 76 | @Post('upload-bpmn') 77 | @ApiConsumes("multipart/form-data") 78 | @ApiBody({ 79 | schema: { 80 | type: "object", 81 | properties: { 82 | file: { 83 | type: "string", 84 | format: "binary", 85 | } 86 | }, 87 | }, 88 | }) 89 | @UseInterceptors(FileInterceptor('file', multerOptions)) 90 | uploadBPMNFile( 91 | @Headers() headers: CommonHeadersDto, 92 | @UploadedFile() file: Express.Multer.File, 93 | ) { 94 | return this.processDefinitionService.uploadBPMNFile(headers, file); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/modules/process-definitions/process-definitions.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus, Injectable, Logger, Scope } from '@nestjs/common'; 2 | import { ProcessDefinition, ProcessDefinitionDocument } from 'src/models/process-definitions/process-definitions.schema'; 3 | import { CreateProcessDefinitionDto, UpdateProcessDefinitionDto, GetProcessDefinitionDto, UpdateDefinitionParams, StageDefinition, GetProcessDefinitionQueryDto } from './dtos'; 4 | import CustomResponse from 'src/common/providers/custom-response.service'; 5 | import CustomError from 'src/common/exceptions/custom-error'; 6 | import { CustomMessages } from 'src/common/const/custom-messages'; 7 | import { ProcessDefinitionRepositoryImpl } from 'src/models/process-definitions/repository/process-definitions.repository.impl'; 8 | import { UpdateStageBody, UpdateStageParams } from './dtos/update-stage.dto'; 9 | import { Constants, LogEntities, Operations, Paths } from 'src/common/const/constants'; 10 | import { Mappings } from 'src/common/const/BPMN-mappings'; 11 | import { CommonHeadersDto } from 'src/shared/dtos'; 12 | import { Compiler } from '../process-instances/providers'; 13 | import { StagesSchema } from './joi-validations/create-process-definition.joi'; 14 | import * as _ from 'lodash'; 15 | import sanitize from 'sanitize-filename'; 16 | 17 | const convert = require('xml-js'); 18 | 19 | @Injectable() 20 | export class ProcessDefinitionService { 21 | private readonly logger: Logger = new Logger(ProcessDefinitionService.name); 22 | 23 | constructor ( 24 | private processDefinitionRepositoryImpl: ProcessDefinitionRepositoryImpl, 25 | private compiler: Compiler, 26 | ) { } 27 | 28 | /** 29 | * checks duplicate key in an array of objects 30 | * @param key {string} - key to be checked, e.g. "k3" 31 | * @param arr {object} - target array, e.g. [{key: "k1"}, {key: "k2"}] 32 | * @returns {boolean} e.g. true/false 33 | */ 34 | 35 | private checkDuplicateKey(key, arr) { 36 | let map = {}; 37 | let isDup = false; 38 | for (let i = 0; i < arr.length; i++) { 39 | let obj = arr[i]; 40 | if (map[obj[key]]) { 41 | isDup = true; 42 | break; 43 | } 44 | map[obj[key]] = true; 45 | } 46 | return isDup; 47 | } 48 | 49 | /** 50 | * It verifies the compound task properties with the properties of the attached process definition 51 | * @param definition {object} - Process definiton object, e.g. {name: "", key: "",properties: [], stages:[], ...} 52 | * @returns {undefined/object} - Error array, e.g. [{taskKey: "", processDefinitionKey: "", parameterKey: "", error: ""}] 53 | */ 54 | private async verifyCompoundTaskParams(definition) { 55 | const compoundTasks = definition.stages.filter(stage => stage.subType === Constants.STAGE_SUB_TYPES.COMPOUND_TASK && (stage.processDefinitionId || stage.processDefinitionKey)); 56 | const childProcessDefinitionKeys = compoundTasks.map(stage => stage.processDefinitionKey); 57 | const compObject = compoundTasks.reduce( 58 | (obj, item) => Object.assign(obj, { [item.processDefinitionKey]: { key: item.key, properties: item.properties } }), {}) 59 | const childDef = await this.processDefinitionRepositoryImpl.find({ key: childProcessDefinitionKeys, properties: { $exists: true, $ne: [] } }, { key: 1, properties: 1 }); 60 | const errors = []; 61 | childDef.forEach(def => { 62 | const propObj = compObject[def.key].properties.reduce( 63 | (obj, item) => Object.assign(obj, { [item.key]: item.value?.default }), {}); 64 | def.properties.forEach(p => { 65 | if ((p.value.required === true || p.value.required as any === 'true') && !propObj[p.key]) { 66 | errors.push({ taskKey: compObject[def.key].key, processDefinitionKey: def.key, parameterKey: p.key, error: 'default value must be set for the input parameters which are required for child workflow' }) 67 | } 68 | }); 69 | 70 | }); 71 | if (errors[0]) { 72 | return errors; 73 | } 74 | } 75 | 76 | /** 77 | * Validate the individual stage schema 78 | * @param createProcessDefinitionDto {object} - Process definiton object, e.g. {name: "", key: "",properties: [], stages:[], ...} 79 | * @returns {undefined/string} - Error, e.g. "Error for key [k1] and subType [user-task] : [Invalid datatype]" 80 | */ 81 | private async validateStages(createProcessDefinitionDto: CreateProcessDefinitionDto) { 82 | if (!createProcessDefinitionDto.stages.length) { 83 | return `The stages array must have at least one element.`; 84 | } 85 | for (let i = 0; i < createProcessDefinitionDto.stages.length; i++) { 86 | const stage = createProcessDefinitionDto.stages[i]; 87 | const stageSubType = stage.subType; 88 | const validationResult = StagesSchema?.[stageSubType]?.validate(stage); 89 | if (validationResult?.error) { 90 | return `Error for key [${stage?.key}] and subType [${stageSubType}] : [${validationResult?.error?.details?.[0]?.message}]`; 91 | } 92 | } 93 | } 94 | 95 | /** 96 | * Create a process definition record in database 97 | * @param headers {object} - Defined request headers 98 | * @param createProcessDefinitionDto {object} - Process definiton object, e.g. {name: "", key: "",properties: [], stages:[], ...} 99 | * @returns {Promise} - CustomError or CustomResponse, e.g. {statusCode: 400, message: "", error: {}}/{statusCode: 200, message: "", result: {}} 100 | */ 101 | async create(headers: CommonHeadersDto, createProcessDefinitionDto: CreateProcessDefinitionDto): Promise { 102 | const uniqueStageIdentifier = 'key'; 103 | if (this.checkDuplicateKey(uniqueStageIdentifier, createProcessDefinitionDto.stages)) { 104 | throw new CustomError(HttpStatus.BAD_REQUEST, `[${uniqueStageIdentifier}] must be unique within [stages] array`); 105 | } 106 | 107 | //validates the process-definition-stages schema 108 | const stagesValidationResult = await this.validateStages(createProcessDefinitionDto); 109 | if (stagesValidationResult) { 110 | throw new CustomError(HttpStatus.BAD_REQUEST, stagesValidationResult); 111 | } 112 | 113 | const compoundTaskValidate = await this.verifyCompoundTaskParams(createProcessDefinitionDto); 114 | if (compoundTaskValidate) { 115 | throw new CustomError(HttpStatus.BAD_REQUEST, 'Error in compound tasks [properties]', compoundTaskValidate); 116 | 117 | } 118 | 119 | let workflow = await this.processDefinitionRepositoryImpl.findOne({ key: createProcessDefinitionDto.key }, {}, { version: -1 }); 120 | if (workflow) { 121 | createProcessDefinitionDto['version'] = workflow.version + 1; 122 | const updatedProcessDefinition = { ...createProcessDefinitionDto, ...workflow }; 123 | const data = await this.processDefinitionRepositoryImpl.create(updatedProcessDefinition); 124 | this.updateCompiledDefinition(data); 125 | return new CustomResponse(HttpStatus.OK, CustomMessages.WORKFLOW_CREATED_NEW_VERSION, data); 126 | } 127 | 128 | createProcessDefinitionDto['version'] = 1; 129 | const data = await this.processDefinitionRepositoryImpl.create(createProcessDefinitionDto); 130 | this.updateCompiledDefinition(data); 131 | return new CustomResponse(HttpStatus.CREATED, CustomMessages.WORKFLOW_CREATED, data); 132 | } 133 | 134 | /** 135 | * Add a compiled process definition in the root process definition 136 | * @param processDefinition {object} - Process definiton object, e.g. {name: "", key: "",properties: [], stages:[], ...} 137 | */ 138 | private async updateCompiledDefinition(processDefinition) { 139 | const compiledWorkflow = this.compiler.compileV2(processDefinition); 140 | await this.processDefinitionRepositoryImpl.update({ _id: processDefinition._id }, { _compiledDefinition: compiledWorkflow }); 141 | } 142 | 143 | /** 144 | * Update a process definition record by id in database 145 | * @param headers {object} - Defined request headers 146 | * @param id {string} - _id of the mongo document 147 | * @param updateProcessDefinitionDto {object} - Process definiton object, e.g. {name: "", key: "",properties: [], stages:[], ...} 148 | * @returns {Promise} - CustomError or CustomResponse, e.g. {statusCode: 400, message: "", error: {}}/{statusCode: 200, message: "", result: {}} 149 | */ 150 | async update(headers: CommonHeadersDto, id: string, updateProcessDefinitionDto: UpdateProcessDefinitionDto): Promise { 151 | const workflow = await this.processDefinitionRepositoryImpl.findOne({ id: id }); 152 | if (!workflow) { 153 | throw new CustomError(HttpStatus.BAD_REQUEST, CustomMessages.WORKFLOW_NOT_FOUND); 154 | } 155 | const uniqueStageIdentifier = 'key'; 156 | if (this.checkDuplicateKey(uniqueStageIdentifier, updateProcessDefinitionDto.stages)) { 157 | throw new CustomError(HttpStatus.BAD_REQUEST, `[${uniqueStageIdentifier}] must be unique within [stages] array`); 158 | } 159 | // updateProcessDefinitionDto['version'] = workflow.version + 1; 160 | const data = await this.processDefinitionRepositoryImpl.update({ _id: id }, updateProcessDefinitionDto); 161 | this.updateCompiledDefinition(data); 162 | return new CustomResponse(HttpStatus.OK, CustomMessages.WORKFLOW_UPDATED, data); 163 | } 164 | 165 | /** 166 | * Update a stage by processDefinitionId and stageId 167 | * @param headers {object} - Defined request headers 168 | * @param params {object} - Defined query params 169 | * @param stage {object} - Stage object 170 | * @returns {Promise} - CustomError or CustomResponse, e.g. {statusCode: 400, message: "", error: {}}/{statusCode: 200, message: "", result: {}} 171 | */ 172 | async updateStage(headers: CommonHeadersDto, params: UpdateStageParams, stage: UpdateStageBody): Promise { 173 | const condition = { _id: params.processDefinitionId, 'stages._id': params.stageId }; 174 | const workflow = await this.processDefinitionRepositoryImpl.findOne(condition); 175 | if (!workflow) { 176 | throw new CustomError(HttpStatus.BAD_REQUEST, CustomMessages.WORKFLOW_NOT_FOUND); 177 | } 178 | const setValues = { 179 | ...(stage.description && { 'stages.$.description': stage.description }), 180 | ...(stage.processDefinitionId && { 'stages.$.processDefinitionId': stage.processDefinitionId }), 181 | ...(stage.conditions?.length && { 'stages.$.conditions': stage.conditions }), 182 | ...(stage.properties?.length && { 'stages.$.properties': stage.properties }), 183 | ...(stage.assignee?.length && { 'stages.$.assignee': stage.assignee }), 184 | ...(stage.criteria && { 'stages.$.criteria': stage.criteria }), 185 | ...(stage.connector && { 'stages.$.connector': stage.connector }), 186 | ...(stage.nextStages && { 'stages.$.nextStages': stage.nextStages }), 187 | ...(stage.type && { 'stages.$.type': stage.type }), 188 | ...(stage.subType && { 'stages.$.subType': stage.subType }) 189 | }; 190 | const data = await this.processDefinitionRepositoryImpl.updateStage(condition, setValues); 191 | return new CustomResponse(HttpStatus.OK, CustomMessages.WORKFLOW_UPDATED, data); 192 | } 193 | 194 | /** 195 | * Find a list of definitions with pagination, filters, search option 196 | * @param query {object} - Query params object 197 | * @returns {Promise} - CustomError or CustomResponse, e.g. {statusCode: 400, message: "", error: {}}/{statusCode: 200, message: "", result: {}} 198 | */ 199 | async findAll(query: GetProcessDefinitionDto): Promise { 200 | let condition: any = { $and: [] }; 201 | let sort = null; 202 | let filters = null; 203 | try { 204 | if (query.filters) { 205 | filters = JSON.parse(query.filters); 206 | condition.$and.push(filters); 207 | } 208 | 209 | 210 | if (query.search) { 211 | const search = JSON.parse(query.search); 212 | // condition.$and = []; 213 | const orCond = { $or: [] }; 214 | for (let key in search) { 215 | if (search.hasOwnProperty(key)) { 216 | let obj = {}; 217 | const safeKey = _.escapeRegExp(search[key]); 218 | obj[key] = { $regex: new RegExp(safeKey, 'i') } 219 | orCond.$or.push(obj); 220 | } 221 | } 222 | condition.$and.push(orCond); 223 | } 224 | 225 | if (query.sort) { 226 | sort = JSON.parse(query.sort); 227 | } 228 | } catch (err) { 229 | this.logger.error(err); 230 | 231 | throw new CustomError(HttpStatus.BAD_REQUEST, ` [filters/customFilters/search] ${CustomMessages.INVALID_JSON}`); 232 | } 233 | if (!condition.$and.length) { 234 | condition = {}; 235 | } 236 | 237 | const count = await this.processDefinitionRepositoryImpl.count(condition); 238 | const data = await this.processDefinitionRepositoryImpl.find(condition, {}, query.page, query.size, sort || { $natural: -1 }); 239 | return new CustomResponse(HttpStatus.OK, CustomMessages.SUCCESS, { count, data }); 240 | } 241 | 242 | /** 243 | * Find a process deifnition by _id 244 | * @param id {string} - _id of the mongo document 245 | * @returns {Promise} - CustomError or CustomResponse, e.g. {statusCode: 400, message: "", error: {}}/{statusCode: 200, message: "", result: {}} 246 | */ 247 | async findOne(id: string): Promise { 248 | let condition = { _id: id }; 249 | const workflow = await this.processDefinitionRepositoryImpl.findOne(condition); 250 | if (!workflow) { 251 | throw new CustomError(HttpStatus.BAD_REQUEST, CustomMessages.WORKFLOW_NOT_FOUND); 252 | } 253 | return new CustomResponse(HttpStatus.OK, CustomMessages.SUCCESS, workflow); 254 | } 255 | 256 | /** 257 | * Find a process deifnition by key 258 | * @param key {string} - Process definiton key 259 | * @param query {object} - Query params object 260 | * @returns {Promise} - CustomError or CustomResponse, e.g. {statusCode: 400, message: "", error: {}}/{statusCode: 200, message: "", result: {}} 261 | */ 262 | async findOneByKey(key: string, query: GetProcessDefinitionQueryDto): Promise { 263 | let condition = { 264 | key, ...(query.version && { version: query.version }), 265 | }; 266 | const workflow = await this.processDefinitionRepositoryImpl.findOne(condition, {}, { version: -1 }); 267 | if (!workflow) { 268 | throw new CustomError(HttpStatus.BAD_REQUEST, CustomMessages.WORKFLOW_NOT_FOUND); 269 | } 270 | return new CustomResponse(HttpStatus.OK, CustomMessages.SUCCESS, workflow); 271 | } 272 | 273 | /** 274 | * Upload a file 275 | * @param headers {object} - Defined request headers 276 | * @param file {object} - file object 277 | * @returns {Promise} - CustomResponse, e.g. {statusCode: 200, message: "", result: {}} 278 | */ 279 | async uploadProtoFile(headers: CommonHeadersDto, file) { 280 | return new CustomResponse(HttpStatus.OK, CustomMessages.SUCCESS, { protoPath: file.filename }); 281 | } 282 | 283 | /** 284 | * Upload a file 285 | * @param headers {object} - Defined request headers 286 | * @param file {object} - file object 287 | * @returns {Promise} - CustomError or CustomResponse, e.g. {statusCode: 400, message: "", error: {}}/{statusCode: 200, message: "", result: {}} 288 | */ 289 | async uploadBPMNFile(headers: CommonHeadersDto, file) { 290 | const filename = sanitize(file.originalname); 291 | const xml = require('fs').readFileSync(`${Paths.BPMN_XML}/${filename}`, 'utf8'); 292 | const options = { ignoreComment: true, alwaysChildren: true }; 293 | const rawJson = convert.xml2js(xml, options); // or convert.xml2json(xml, options) 294 | const businessFlowJson = this.bpmnJsonToNativeJson(rawJson); 295 | return this.create(headers, businessFlowJson); 296 | } 297 | 298 | /** 299 | * Convert bpmn json to native json 300 | * @param bpmnJson {object} - input bpmn json 301 | * @returns {object} - target json 302 | */ 303 | bpmnJsonToNativeJson(bpmnJson) { 304 | 305 | let bpmnProcessJson = bpmnJson.elements[0]['elements'][0]; 306 | let targetProcessJSON: CreateProcessDefinitionDto = { 307 | name: bpmnProcessJson.attributes.name || bpmnProcessJson.attributes.id, 308 | description: bpmnProcessJson.attributes.description || '', 309 | key: bpmnProcessJson.attributes.id, 310 | isParallel: false, 311 | stages: [], 312 | properties: [] 313 | } 314 | targetProcessJSON.stages = bpmnProcessJson.elements.map(ele => { 315 | if (Mappings.StageMappings[ele.name]) { 316 | let stage = { ...Mappings.StageMappings[ele.name] }; 317 | stage.key = ele?.attributes.id; 318 | stage.name = ele?.attributes.name || ele?.attributes.id; 319 | stage.description = ele.attributes.description || ele.name; 320 | const nextStages = bpmnProcessJson.elements.filter(linkObj => linkObj.name === 'bpmn:sequenceFlow' && linkObj.attributes.sourceRef === ele.attributes.id).map(obj => obj.attributes.targetRef); 321 | stage.properties = ele.elements.find(obj => obj.name === 'bpmn:extensionElements')?.elements.find(obj => obj.name === 'zeebe:ioMapping')?.elements.map(obj => { 322 | if (obj.name === 'zeebe:input') { 323 | return { 324 | key: obj.attributes.target, 325 | value: { 326 | type: 'string', 327 | default: '' 328 | } 329 | } 330 | } 331 | }).filter(obj => obj); 332 | 333 | stage.nextStages = nextStages; 334 | if (ele.name === 'bpmn:callActivity') { 335 | stage.processDefinitionKey = ele.elements.find(extEle => extEle.name = 'bpmn:extensionElements')?.elements.find(obj => obj.name === 'zeebe:calledElement')?.attributes?.processId; 336 | } 337 | return stage; 338 | } 339 | }).filter(obj => obj?.key); 340 | return targetProcessJSON; 341 | } 342 | 343 | } 344 | -------------------------------------------------------------------------------- /src/modules/process-definitions/providers/multer-options.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, mkdirSync } from 'fs'; 2 | import { diskStorage, memoryStorage } from 'multer'; 3 | import { Paths } from 'src/common/const/constants'; 4 | 5 | export const multerConfig = { 6 | proto: Paths.WORKFLOW_PROTO_DIR, 7 | bpmn: Paths.BPMN_XML 8 | }; 9 | 10 | export const assignStorageProperty = { 11 | storageProperty: '' 12 | } 13 | const kebabCase = string => string 14 | .replace(/([a-z])([A-Z])/g, "$1-$2") 15 | .replace(/[\s_]+/g, '-') 16 | .toLowerCase(); 17 | 18 | const storageOption = 19 | { 20 | memorystorage: memoryStorage(), 21 | diskstorage: diskStorage({ 22 | // Destination storage path details 23 | destination: (req: any, file: any, cb: any) => { 24 | 25 | let uploadPath = Paths.PUBLIC; 26 | const ext = file.originalname.split('.').pop(); 27 | switch (ext) { 28 | case 'bpmn': 29 | uploadPath = Paths.BPMN_XML; 30 | break; 31 | case 'proto': 32 | uploadPath = Paths.WORKFLOW_PROTO_DIR; 33 | break; 34 | default: 35 | 36 | break; 37 | } 38 | // Create folder if doesn't exist 39 | if (!existsSync(uploadPath)) { 40 | mkdirSync(uploadPath); 41 | } 42 | cb(null, uploadPath); 43 | }, 44 | // File modification details 45 | filename: (req: any, file: any, cb: any) => { 46 | // Calling the callback passing the random name generated with the original extension name 47 | 48 | let fileName = kebabCase(file.originalname); 49 | 50 | cb(null, fileName); 51 | 52 | }, 53 | }), 54 | } 55 | 56 | 57 | export const multerOptions = { 58 | 59 | // Storage properties 60 | storage: storageOption['diskstorage'] 61 | 62 | }; 63 | // 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/modules/process-instances/dtos/create-process-instance.rest.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; 2 | import { 3 | IsString, 4 | IsNotEmpty, 5 | IsNotEmptyObject, 6 | IsInt, 7 | IsBoolean, 8 | ValidateNested, 9 | IsDate, 10 | IsDateString, 11 | IsEmail, 12 | IsObject, 13 | IsArray, 14 | IsNumber, 15 | IsOptional, 16 | IsIn, 17 | IsEnum, 18 | Min 19 | } from 'class-validator'; 20 | import { Type } from 'class-transformer'; 21 | 22 | class Params { 23 | [key: string]: any; 24 | } 25 | 26 | export class CreateProcessInstanceDto { 27 | @ApiProperty() 28 | @IsOptional() 29 | @Type(() => Params) 30 | parameters: Params; 31 | 32 | 33 | } 34 | 35 | export class GetOneProcessInstanceQueryDto { 36 | @ApiPropertyOptional() 37 | @IsNumber() 38 | @IsOptional() 39 | @Min(1) 40 | @Type(() => Number) 41 | readonly version: number; 42 | 43 | } -------------------------------------------------------------------------------- /src/modules/process-instances/dtos/get-process-instances.rest.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsOptional, IsNumber } from 'class-validator'; 2 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; 3 | import { Type } from 'class-transformer'; 4 | 5 | export class GetProcessInstanceQueryDto { 6 | @ApiProperty({ 7 | default: 0, 8 | }) 9 | @IsNumber() 10 | @Type(() => Number) 11 | readonly page: number; 12 | 13 | @ApiProperty({ 14 | default: 10, 15 | }) 16 | @IsNumber() 17 | @Type(() => Number) 18 | readonly size: number; 19 | 20 | @IsString() 21 | @ApiPropertyOptional() 22 | @IsOptional() 23 | readonly filters: string; 24 | 25 | @IsString() 26 | @ApiPropertyOptional() 27 | @IsOptional() 28 | readonly customFilters: string; 29 | 30 | @IsString() 31 | @ApiPropertyOptional() 32 | @IsOptional() 33 | readonly sort: string; 34 | 35 | @IsString() 36 | @ApiPropertyOptional() 37 | @IsOptional() 38 | readonly search: string; 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/modules/process-instances/dtos/index.ts: -------------------------------------------------------------------------------- 1 | export * from './process-instance.dto' 2 | export * from './get-process-instances.rest.dto' 3 | export * from './update-process-instance.dto' 4 | export * from './create-process-instance.rest.dto' 5 | export * from './start-process-instance.rest.dto' 6 | export * from './stats.rest.dto' -------------------------------------------------------------------------------- /src/modules/process-instances/dtos/process-instance.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { 3 | IsString, 4 | IsNotEmpty, 5 | IsNotEmptyObject, 6 | IsInt, 7 | IsBoolean, 8 | ValidateNested, 9 | IsDate, 10 | IsDateString, 11 | IsEmail, 12 | IsObject, 13 | IsArray, 14 | IsNumber, 15 | IsOptional, 16 | IsIn, 17 | IsEnum 18 | } from 'class-validator'; 19 | import { Type } from 'class-transformer'; 20 | import { StageTypes, StageSubTypes } from 'src/common/const/enums'; 21 | import { CreateProcessDefinitionDto } from 'src/modules/process-definitions/dtos' 22 | 23 | export class ProcessInstanceDto extends CreateProcessDefinitionDto { 24 | 25 | @IsOptional() 26 | timeActivated: number; 27 | 28 | @IsOptional() 29 | timeStarted: number; 30 | 31 | @IsOptional() 32 | timeCompleted: number; 33 | 34 | @IsOptional() 35 | status: string; 36 | 37 | } -------------------------------------------------------------------------------- /src/modules/process-instances/dtos/start-process-instance.rest.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { 3 | IsString, 4 | IsNotEmpty, 5 | IsNotEmptyObject, 6 | IsInt, 7 | IsBoolean, 8 | ValidateNested, 9 | IsDate, 10 | IsDateString, 11 | IsEmail, 12 | IsObject, 13 | IsArray, 14 | IsNumber, 15 | IsOptional, 16 | IsIn, 17 | IsEnum 18 | } from 'class-validator'; 19 | import { Type } from 'class-transformer'; 20 | 21 | class Params { 22 | [key: string]: any; 23 | } 24 | 25 | export class StartProcessInstanceBodyDto { 26 | @ApiProperty() 27 | @IsOptional() 28 | @Type(() => Params) 29 | parameters: Params; 30 | 31 | 32 | } -------------------------------------------------------------------------------- /src/modules/process-instances/dtos/stats.rest.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsOptional, IsNumber, Min } from 'class-validator'; 2 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; 3 | import { Type } from 'class-transformer'; 4 | 5 | export class GetProcessInstanceStatsQueryDto { 6 | 7 | @IsString() 8 | @ApiPropertyOptional() 9 | @IsOptional() 10 | readonly filters: string; 11 | 12 | @IsString() 13 | @ApiPropertyOptional() 14 | @IsOptional() 15 | readonly customFilters: string; 16 | 17 | @IsString() 18 | @ApiPropertyOptional() 19 | @IsOptional() 20 | readonly search: string; 21 | 22 | @ApiPropertyOptional() 23 | @IsNumber() 24 | @IsOptional() 25 | @Min(1) 26 | @Type(() => Number) 27 | readonly version?: number; 28 | } 29 | -------------------------------------------------------------------------------- /src/modules/process-instances/dtos/update-process-instance.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; 2 | import { 3 | IsBoolean, 4 | IsOptional, 5 | IsString 6 | } from 'class-validator' 7 | import { Transform, Type } from 'class-transformer'; 8 | 9 | class Params { 10 | [key: string]: any; 11 | } 12 | 13 | export class UpdateProcessInstanceDto { 14 | @ApiProperty() 15 | @IsOptional() 16 | @Type(() => Params) 17 | parameters: Params; 18 | } 19 | 20 | export class UpdateProcessInstanceQueryDto { 21 | @IsString() 22 | @IsOptional() 23 | @ApiPropertyOptional({ default: false }) 24 | readonly cascade: string; 25 | } -------------------------------------------------------------------------------- /src/modules/process-instances/process-instances.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { HttpModule, HttpService } from '@nestjs/axios' 3 | 4 | import { ProcessInstance, ProcessInstanceSchema } from 'src/models/process-instances/process-instances.schema'; 5 | 6 | import { ProcessInstanceRepositoryImpl } from 'src/models/process-instances/repository/process-instances.repository.impl'; 7 | import { ProcessDefinitionRepositoryImpl } from 'src/models/process-definitions/repository/process-definitions.repository.impl'; 8 | 9 | import { ProcessInstanceController } from './process-instances.rest.controller'; 10 | import { ProcessInstanceService } from './process-instances.service'; 11 | import { Compiler } from './providers'; 12 | import { Executor } from '../tasks/providers'; 13 | import { HttpConnector, GrpcConnector, OpenAIConnector } from 'src/shared/connectors'; 14 | import { UserTasksRepositoryImpl } from 'src/models/user-tasks/repository/user-tasks.repository.impl'; 15 | import { MongooseModule } from '@nestjs/mongoose'; 16 | import { ProcessDefinition, ProcessDefinitionSchema } from 'src/models/process-definitions/process-definitions.schema'; 17 | import { UserTasks, UserTasksSchema } from 'src/models/user-tasks/user-tasks.schema'; 18 | 19 | @Module({ 20 | imports: [ 21 | MongooseModule.forFeature([{ name: ProcessInstance.name, schema: ProcessInstanceSchema }, { name: ProcessDefinition.name, schema: ProcessDefinitionSchema }, { name: UserTasks.name, schema: UserTasksSchema }]), 22 | HttpModule 23 | 24 | ], 25 | controllers: [ProcessInstanceController], 26 | providers: [ProcessInstanceService, ProcessInstanceRepositoryImpl, ProcessDefinitionRepositoryImpl, Compiler, Executor, UserTasksRepositoryImpl, HttpConnector, GrpcConnector, OpenAIConnector], 27 | }) 28 | export class ProcessInstancesModule { } 29 | -------------------------------------------------------------------------------- /src/modules/process-instances/process-instances.rest.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Put, Query, Headers, Patch, UseGuards } from '@nestjs/common'; 2 | import { ApiHeader, ApiTags } from '@nestjs/swagger'; 3 | import CustomResponse from 'src/common/providers/custom-response.service'; 4 | import CustomError from 'src/common/exceptions/custom-error'; 5 | import { ProcessInstance, ProcessInstanceDocument } from 'src/models/process-instances/process-instances.schema'; 6 | import { CreateProcessInstanceDto, GetProcessInstanceStatsQueryDto, GetProcessInstanceQueryDto, GetOneProcessInstanceQueryDto, UpdateProcessInstanceDto, UpdateProcessInstanceQueryDto } from './dtos'; 7 | import { ProcessInstanceService } from './process-instances.service'; 8 | import { CommonHeadersDto } from 'src/shared/dtos'; 9 | @ApiTags('Process Instances') 10 | @Controller('process-instances') 11 | 12 | export class ProcessInstanceController { 13 | constructor (private processInstanceService: ProcessInstanceService) { } 14 | 15 | @Post(':processDefinitionId') 16 | async createByDefinitionId(@Headers() headers: CommonHeadersDto, @Param('processDefinitionId') processDefinitionId: string): Promise { 17 | return this.processInstanceService.createByDefinitionId(headers, processDefinitionId); 18 | } 19 | 20 | @Post('key/:key') 21 | async createByDefinitionKey(@Headers() headers: CommonHeadersDto, @Param('key') key: string, @Query() query: GetOneProcessInstanceQueryDto): Promise { 22 | return this.processInstanceService.createByDefinitionKey(headers, query, key); 23 | } 24 | 25 | @Post(':processInstanceId/start') 26 | async start(@Headers() headers: CommonHeadersDto, @Param('processInstanceId') processInstanceId: string, @Body() createProcessInstanceDto: CreateProcessInstanceDto): Promise { 27 | return this.processInstanceService.start(processInstanceId, createProcessInstanceDto); 28 | } 29 | 30 | @Get(':processDefinitionId/list') 31 | async findAll(@Headers() headers: CommonHeadersDto, @Param('processDefinitionId') processDefinitionId: string, @Query() getProcessInstanceDto: GetProcessInstanceQueryDto): Promise { 32 | return this.processInstanceService.getInstancesByDefinitionId(processDefinitionId, getProcessInstanceDto); 33 | } 34 | 35 | @Get('key/:key/list') 36 | async findAllByKey(@Headers() headers: CommonHeadersDto, @Param('key') processDefinitionKey: string, @Query() getProcessInstanceDto: GetProcessInstanceQueryDto): Promise { 37 | return this.processInstanceService.getInstancesByDefinitionKey(processDefinitionKey, getProcessInstanceDto); 38 | } 39 | 40 | @Get(':processInstanceId') 41 | async findOne(@Headers() headers: CommonHeadersDto, @Param('processInstanceId') id: string): Promise { 42 | return this.processInstanceService.findOne(id); 43 | } 44 | 45 | @Get('stats/:processDefinitionId') 46 | async getStatsByDefinitionId(@Headers() headers: CommonHeadersDto, @Param('processDefinitionId') processDefinitionId: string, @Query() query: GetProcessInstanceStatsQueryDto): Promise { 47 | return this.processInstanceService.getStatsByDefinitionId(processDefinitionId, query); 48 | } 49 | 50 | @Get('stats/key/:key') 51 | async getStatsByDefinitionKey(@Headers() headers: CommonHeadersDto, @Param('key') processDefinitionKey: string, @Query() query: GetProcessInstanceStatsQueryDto): Promise { 52 | return this.processInstanceService.getStatsByDefinitionKey(processDefinitionKey, query); 53 | } 54 | @Patch(':processInstanceId') 55 | async updateInstances(@Headers() headers: CommonHeadersDto, @Param('processInstanceId') processInstanceId: string, @Body() updateProcessInstanceDto: UpdateProcessInstanceDto, @Query() query: UpdateProcessInstanceQueryDto): 56 | Promise { 57 | return this.processInstanceService.updateInstances(processInstanceId, updateProcessInstanceDto, query); 58 | } 59 | 60 | @Post('run/:processDefinitionId') 61 | async runByDefinitionId(@Headers() headers: CommonHeadersDto, @Param('processDefinitionId') processDefinitionId: string, @Body() createProcessInstanceDto: CreateProcessInstanceDto): Promise { 62 | return this.processInstanceService.runByDefinitionId(headers, processDefinitionId, createProcessInstanceDto); 63 | } 64 | 65 | @Post('run/key/:key') 66 | async runByDefinitionKey(@Headers() headers: CommonHeadersDto, @Param('key') key: string, @Query() query: GetOneProcessInstanceQueryDto, @Body() createProcessInstanceDto: CreateProcessInstanceDto): Promise { 67 | return this.processInstanceService.runByDefinitionKey(headers, query, key, createProcessInstanceDto); 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /src/modules/process-instances/providers/compiler.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus, Logger, Injectable } from "@nestjs/common"; 2 | import { Types } from 'mongoose'; 3 | import { Constants } from "src/common/const/constants"; 4 | 5 | 6 | // import { Executor } from "src/modules/tasks/providers/executor"; 7 | 8 | @Injectable() 9 | export class Compiler { 10 | private readonly logger: Logger = new Logger(); 11 | constructor () { } 12 | 13 | compile(processDefinition, rootProcessInstanceId?, parentProcessInstanceId?, parentTaskId?) { 14 | const now = Date.now(); 15 | 16 | let compiledProcessDefinition = JSON.parse(JSON.stringify(processDefinition)); 17 | 18 | // Check if start stage is present, check for multiple stages 19 | const startIndex = compiledProcessDefinition.stages.findIndex(stage => stage.subType === Constants.STAGE_SUB_TYPES.START); 20 | const endIndex = compiledProcessDefinition.stages.findIndex(stage => stage.subType === Constants.STAGE_SUB_TYPES.END); 21 | 22 | compiledProcessDefinition._startIndex = startIndex; 23 | compiledProcessDefinition._endIndex = endIndex; 24 | 25 | if (startIndex !== -1) { 26 | compiledProcessDefinition.stages[startIndex].status = Constants.STAGE_STATUSES.ACTIVE; // status of start stage 27 | compiledProcessDefinition.stages[startIndex].timeActivated = now;; // status of start stage 28 | } 29 | 30 | const stageIndexJSON = {}; 31 | compiledProcessDefinition.processDefinitionId = compiledProcessDefinition._id; 32 | delete compiledProcessDefinition.__v; 33 | // delete processDefinitionId 34 | delete compiledProcessDefinition._id; 35 | delete compiledProcessDefinition._compiledDefinition; 36 | 37 | compiledProcessDefinition.processDefinitionKey = compiledProcessDefinition.key; 38 | compiledProcessDefinition.parentProcessInstanceId = parentProcessInstanceId; 39 | 40 | compiledProcessDefinition.parentTaskId = parentTaskId; 41 | // generate processInstanceId 42 | compiledProcessDefinition._id = new Types.ObjectId(); 43 | compiledProcessDefinition.status = Constants.STAGE_STATUSES.ACTIVE; 44 | compiledProcessDefinition.timeActivated = now; 45 | compiledProcessDefinition.timeStarted = now; 46 | compiledProcessDefinition.rootProcessInstanceId = rootProcessInstanceId ? rootProcessInstanceId : compiledProcessDefinition._id.valueOf(); 47 | 48 | 49 | 50 | // compiledProcessDefinition.parameters = parameters; // assign input parameters 51 | // let skipStages = []; 52 | // if (compiledProcessDefinition.isParallel) { 53 | // // remove start and end stage 54 | // // compiledProcessDefinition.stages = compiledProcessDefinition.stages.filter(stage => (stage.subType !== 'start' && stage.subType !== 'end')); 55 | 56 | // skipStages = compiledProcessDefinition.stages.map(obj => obj.nextStage).filter(stage => stage); 57 | // } 58 | compiledProcessDefinition.stages = compiledProcessDefinition.stages.map((task, index) => { 59 | stageIndexJSON[task.key] = index; // create JSON for keys and indexes 60 | task.stageId = task._id; 61 | delete task._id; 62 | // skip already active task 63 | if (task.status !== Constants.STAGE_STATUSES.ACTIVE) { 64 | 65 | // in case of parallel tasks skip the stages whose keys are present in other task's nextStage 66 | // if (task.type === Constants.STAGE_TYPES.ACTIVITY && skipStages.length && !skipStages.includes(task.key)) { 67 | // task.status = Constants.STAGE_STATUSES.ACTIVE; 68 | // task.timeActivated = now; 69 | // } else { //if (index !== startIndex) { 70 | task.status = Constants.STAGE_STATUSES.WAITING; 71 | // } 72 | } 73 | return task; 74 | }); 75 | compiledProcessDefinition._stageIndexJSON = stageIndexJSON; 76 | return compiledProcessDefinition; 77 | } 78 | 79 | compileV2(processDefinition) { 80 | const now = Date.now(); 81 | 82 | let compiledProcessDefinition = JSON.parse(JSON.stringify(processDefinition)); 83 | 84 | // Check if start stage is present, check for multiple stages 85 | const startIndex = compiledProcessDefinition.stages.findIndex(stage => stage.subType === Constants.STAGE_SUB_TYPES.START); 86 | const endIndex = compiledProcessDefinition.stages.findIndex(stage => stage.subType === Constants.STAGE_SUB_TYPES.END); 87 | 88 | compiledProcessDefinition._startIndex = startIndex; 89 | compiledProcessDefinition._endIndex = endIndex; 90 | 91 | if (startIndex !== -1) { 92 | compiledProcessDefinition.stages[startIndex].status = Constants.STAGE_STATUSES.ACTIVE; // status of start stage 93 | compiledProcessDefinition.stages[startIndex].timeActivated = now;; // status of start stage 94 | } 95 | 96 | const stageIndexJSON = {}; 97 | compiledProcessDefinition.processDefinitionId = compiledProcessDefinition._id; 98 | delete compiledProcessDefinition.__v; 99 | // delete processDefinitionId 100 | delete compiledProcessDefinition._id; 101 | delete compiledProcessDefinition._compiledDefinition; 102 | delete compiledProcessDefinition.createdAt; 103 | delete compiledProcessDefinition.updatedAt; 104 | 105 | compiledProcessDefinition.processDefinitionKey = compiledProcessDefinition.key; 106 | compiledProcessDefinition.parentProcessInstanceId = null; 107 | compiledProcessDefinition.parentTaskId = null; 108 | // generate processInstanceId 109 | compiledProcessDefinition._id = null; 110 | compiledProcessDefinition.status = Constants.STAGE_STATUSES.ACTIVE; 111 | compiledProcessDefinition.timeActivated = now; 112 | compiledProcessDefinition.timeStarted = now; 113 | compiledProcessDefinition.rootProcessInstanceId = null; 114 | 115 | 116 | 117 | // compiledProcessDefinition.parameters = parameters; // assign input parameters 118 | // let skipStages = []; 119 | // if (compiledProcessDefinition.isParallel) { 120 | // // remove start and end stage 121 | // // compiledProcessDefinition.stages = compiledProcessDefinition.stages.filter(stage => (stage.subType !== 'start' && stage.subType !== 'end')); 122 | 123 | // skipStages = compiledProcessDefinition.stages.map(obj => obj.nextStage).filter(stage => stage); 124 | // } 125 | compiledProcessDefinition.stages = compiledProcessDefinition.stages.map((task, index) => { 126 | stageIndexJSON[task.key] = index; // create JSON for keys and indexes 127 | task.stageId = task._id; 128 | delete task._id; 129 | // skip already active task 130 | if (task.status !== Constants.STAGE_STATUSES.ACTIVE) { 131 | 132 | // in case of parallel tasks skip the stages whose keys are present in other task's nextStage 133 | // if (task.type === Constants.STAGE_TYPES.ACTIVITY && skipStages.length && !skipStages.includes(task.key)) { 134 | // task.status = Constants.STAGE_STATUSES.ACTIVE; 135 | // task.timeActivated = now; 136 | // } else { //if (index !== startIndex) { 137 | task.status = Constants.STAGE_STATUSES.WAITING; 138 | // } 139 | } 140 | return task; 141 | }); 142 | compiledProcessDefinition._stageIndexJSON = stageIndexJSON; 143 | return compiledProcessDefinition; 144 | } 145 | private verifyParams(properties, inputParams) { 146 | // validate input params against the properties 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/modules/process-instances/providers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './compiler' -------------------------------------------------------------------------------- /src/modules/tasks/dtos/complete-task.rest.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; 2 | import { 3 | IsEnum, 4 | IsOptional, isString, IsString, Length 5 | } from 'class-validator'; 6 | import { Type } from 'class-transformer'; 7 | import { ConnectorTypes, InputStatuses } from 'src/common/const/enums'; 8 | 9 | class Params { 10 | [key: string]: any; 11 | } 12 | 13 | export class CompleteTaskBody { 14 | @ApiProperty() 15 | @IsOptional() 16 | @Length(24, 24) 17 | taskId: string; 18 | 19 | @ApiProperty() 20 | @IsOptional() 21 | @Length(1, 24) 22 | taskKey: string; 23 | 24 | @ApiProperty({ type: String, default: '' }) 25 | @IsEnum(InputStatuses) 26 | @IsOptional() 27 | status?: InputStatuses; 28 | 29 | @ApiProperty({ type: Params }) 30 | @IsOptional() 31 | // @Type(() => Params) 32 | parameters: Params; 33 | } 34 | 35 | export class CompleteTaskParams { 36 | @ApiProperty() 37 | @IsString() 38 | @Length(24, 24) 39 | processInstanceId: string; 40 | 41 | } 42 | 43 | export class CompleteTaskQuery { 44 | @ApiProperty() 45 | @IsOptional() 46 | @Type(() => Params) 47 | parameters: Params; 48 | } 49 | 50 | export class CompleteTasksHeadersDto { 51 | @IsString() 52 | @ApiPropertyOptional() 53 | @IsOptional() 54 | readonly 'x-tenant-id': string; 55 | } 56 | 57 | -------------------------------------------------------------------------------- /src/modules/tasks/dtos/create-user-task.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { 3 | IsOptional 4 | } from 'class-validator'; 5 | import { Type } from 'class-transformer'; 6 | 7 | class PropertyType { 8 | type: string; 9 | 10 | default: string; 11 | 12 | required: boolean; 13 | 14 | enum: [any]; 15 | } 16 | 17 | class Property { 18 | key: string; 19 | 20 | value: PropertyType 21 | } 22 | 23 | class Params { 24 | [key: string]: any; 25 | } 26 | 27 | export class CreateUserTaskDto { 28 | rootProcessInstanceId: string; 29 | processInstanceId: string; 30 | taskId: string; 31 | assignee?: string; 32 | summary: string; 33 | description: string; 34 | expStartDate: number; 35 | expEndDate: number; 36 | priority: string; 37 | properties: Property[]; 38 | parameters: Params; 39 | watchers?: string[]; 40 | } 41 | -------------------------------------------------------------------------------- /src/modules/tasks/dtos/get-mytasks.rest.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsOptional, IsNumber, Length } from 'class-validator'; 2 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; 3 | import { Type } from 'class-transformer'; 4 | 5 | export class GetMyTasksQueryDto { 6 | @ApiProperty({ 7 | default: 0, 8 | }) 9 | @IsNumber() 10 | @Type(() => Number) 11 | readonly page: number; 12 | 13 | @ApiProperty({ 14 | default: 10, 15 | }) 16 | @IsNumber() 17 | @Type(() => Number) 18 | readonly size: number; 19 | 20 | @IsString() 21 | @ApiPropertyOptional() 22 | @IsOptional() 23 | readonly filters: string; 24 | 25 | @IsString() 26 | @ApiPropertyOptional() 27 | @IsOptional() 28 | readonly customFilters: string; 29 | 30 | @IsString() 31 | @ApiPropertyOptional() 32 | @IsOptional() 33 | readonly sort: string; 34 | 35 | @IsString() 36 | @ApiPropertyOptional() 37 | @IsOptional() 38 | readonly search: string; 39 | } 40 | 41 | export class GetMyTasksHeadersDto { 42 | @IsString() 43 | @ApiPropertyOptional() 44 | @IsOptional() 45 | readonly 'x-tenant-id': string; 46 | 47 | @IsString() 48 | @ApiPropertyOptional() 49 | @IsOptional() 50 | readonly 'user-id': string; 51 | } 52 | 53 | export class GetMyTasksParamsDto { 54 | @IsString() 55 | @ApiPropertyOptional() 56 | @Length(24, 24) 57 | readonly processInstanceId: string; 58 | } 59 | 60 | export class Filters { 61 | status?: string; 62 | startDate?: number; 63 | endDate?: number; 64 | } 65 | -------------------------------------------------------------------------------- /src/modules/tasks/dtos/get-tasks.rest.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsOptional, IsNumber, Length } from 'class-validator'; 2 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; 3 | import { Type } from 'class-transformer'; 4 | 5 | export class GetTasksQueryDto { 6 | @ApiProperty({ 7 | default: 0, 8 | }) 9 | @IsNumber() 10 | @Type(() => Number) 11 | readonly page: number; 12 | 13 | @ApiProperty({ 14 | default: 10, 15 | }) 16 | @IsNumber() 17 | @Type(() => Number) 18 | readonly size: number; 19 | 20 | @IsString() 21 | @ApiPropertyOptional() 22 | @IsOptional() 23 | readonly filters: string; 24 | 25 | @IsString() 26 | @ApiPropertyOptional() 27 | @IsOptional() 28 | readonly customFilters: string; 29 | 30 | @IsString() 31 | @ApiPropertyOptional() 32 | @IsOptional() 33 | readonly sort: string; 34 | 35 | @IsString() 36 | @ApiPropertyOptional() 37 | @IsOptional() 38 | readonly search: string; 39 | } 40 | 41 | export class GetTasksHeadersDto { 42 | @IsString() 43 | @ApiPropertyOptional() 44 | @IsOptional() 45 | readonly 'x-tenant-id': string; 46 | } 47 | 48 | export class GetTasksParamsDto { 49 | @IsString() 50 | @ApiProperty() 51 | @Length(24, 24) 52 | readonly processInstanceId: string; 53 | 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/modules/tasks/dtos/index.ts: -------------------------------------------------------------------------------- 1 | export * from './get-tasks.rest.dto' 2 | export * from './update-user-task.dto' 3 | export * from './complete-task.rest.dto' 4 | export * from './create-user-task.dto' 5 | export * from './get-mytasks.rest.dto' 6 | -------------------------------------------------------------------------------- /src/modules/tasks/dtos/update-user-task.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { 3 | IsString, 4 | IsNotEmpty, 5 | IsInt, 6 | IsBoolean, 7 | ValidateNested, 8 | IsDate, 9 | IsDateString, 10 | IsEmail, 11 | IsObject, 12 | IsArray, 13 | IsNumber, 14 | IsOptional, 15 | IsIn, 16 | Length, 17 | } from 'class-validator' 18 | 19 | class PropertiesDto { 20 | 21 | } 22 | 23 | class StagesDto { 24 | 25 | } 26 | 27 | export class UpdateUserTaskDto { 28 | @IsOptional() 29 | @ApiProperty() 30 | description: string; 31 | 32 | // @IsObject() 33 | // @ApiProperty() 34 | // @IsOptional() 35 | // properties: Record; 36 | 37 | @IsObject() 38 | @ApiProperty() 39 | @IsOptional() 40 | stages: Record; 41 | 42 | } 43 | 44 | class Params { 45 | [key: string]: any; 46 | } 47 | 48 | export class ReAssignTaskBody { 49 | @ApiProperty() 50 | @IsOptional() 51 | @Length(24, 24) 52 | taskId: string; 53 | 54 | @ApiProperty() 55 | @IsOptional() 56 | @Length(1, 24) 57 | taskKey: string; 58 | 59 | @ApiProperty() 60 | @IsOptional() 61 | assignee: string; 62 | 63 | @ApiProperty() 64 | @IsOptional() 65 | watchers: string[]; 66 | 67 | @ApiProperty({ type: Params }) 68 | @IsOptional() 69 | // @Type(() => Params) 70 | parameters: Params; 71 | 72 | } -------------------------------------------------------------------------------- /src/modules/tasks/providers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './executor' 2 | -------------------------------------------------------------------------------- /src/modules/tasks/tasks.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, OnModuleInit } from '@nestjs/common'; 2 | import { HttpModule } from '@nestjs/axios' 3 | import { UserTasks, UserTasksSchema } from 'src/models/user-tasks/user-tasks.schema'; 4 | import { UserTasksRepositoryImpl } from 'src/models/user-tasks/repository/user-tasks.repository.impl'; 5 | import { TasksController } from './tasks.rest.controller'; 6 | import { TasksService } from './tasks.service'; 7 | import { ProcessInstanceRepositoryImpl } from 'src/models/process-instances/repository/process-instances.repository.impl'; 8 | import { ProcessDefinitionRepositoryImpl } from 'src/models/process-definitions/repository/process-definitions.repository.impl'; 9 | import { Compiler } from '../process-instances/providers'; 10 | import { Executor } from './providers'; 11 | import { HttpConnector, GrpcConnector, OpenAIConnector } from 'src/shared/connectors'; 12 | import { MongooseModule } from '@nestjs/mongoose'; 13 | import { ProcessInstance, ProcessInstanceSchema } from 'src/models/process-instances/process-instances.schema'; 14 | import { ProcessDefinition, ProcessDefinitionSchema } from 'src/models/process-definitions/process-definitions.schema'; 15 | 16 | @Module({ 17 | imports: [ 18 | MongooseModule.forFeature([{ name: UserTasks.name, schema: UserTasksSchema }, { name: ProcessInstance.name, schema: ProcessInstanceSchema }, { name: ProcessDefinition.name, schema: ProcessDefinitionSchema }]), 19 | HttpModule, 20 | ], 21 | exports: [], 22 | controllers: [TasksController], 23 | providers: [TasksService, UserTasksRepositoryImpl, ProcessInstanceRepositoryImpl, ProcessDefinitionRepositoryImpl, HttpConnector, Compiler, Executor, GrpcConnector,OpenAIConnector], 24 | }) 25 | export class TasksModule { } 26 | -------------------------------------------------------------------------------- /src/modules/tasks/tasks.rest.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Headers, HttpCode, HttpStatus, Param, Post, Put, Query, Req, UseGuards } from '@nestjs/common'; 2 | import { ApiExcludeEndpoint, ApiHeader, ApiTags } from '@nestjs/swagger'; 3 | import CustomResponse from 'src/common/providers/custom-response.service'; 4 | import CustomError from 'src/common/exceptions/custom-error'; 5 | import { CompleteTaskBody, CompleteTaskParams, CompleteTasksHeadersDto, GetTasksQueryDto, GetTasksHeadersDto, GetTasksParamsDto, ReAssignTaskBody } from './dtos'; 6 | import { CommonHeadersDto } from 'src/shared/dtos'; 7 | import { TasksService } from './tasks.service'; 8 | import { Request } from 'express'; 9 | import { GetProcessInstanceStatsQueryDto } from '../process-instances/dtos'; 10 | @ApiTags('Tasks') 11 | @Controller() 12 | 13 | export class TasksController { 14 | constructor (private tasksService: TasksService) { } 15 | 16 | @Get('my-tasks') 17 | async findAllUserAssignedTasks(@Headers() headers: CommonHeadersDto, @Query() getUserTaskDto: GetTasksQueryDto): Promise { 18 | return this.tasksService.findAllUserAssignedTasks(headers, getUserTaskDto); 19 | } 20 | 21 | @Get('tasks') 22 | async findTasks(@Headers() headers: CommonHeadersDto, @Query() query: GetTasksQueryDto): Promise { 23 | return this.tasksService.findTasks(headers, query); 24 | } 25 | 26 | // @Put('tasks/:processInstanceId/:taskId') 27 | // async updateOne(@Param('id') id: string, @Body() updateUserTaskDto: UpdateUserTaskDto): 28 | // Promise { 29 | // return this.tasksService.updateTask(id, updateUserTaskDto); 30 | // } 31 | 32 | @Post('tasks/:processInstanceId/complete') 33 | async completeTask(@Headers() headers: CommonHeadersDto, @Param() params: CompleteTaskParams, @Body() completeTaskDto: CompleteTaskBody): 34 | Promise { 35 | return this.tasksService.completeTask(headers, params, completeTaskDto); 36 | } 37 | 38 | @Post('tasks/:processInstanceId/reassign') 39 | async reassignTask(@Headers() headers: CommonHeadersDto, @Param() params: CompleteTaskParams, @Body() completeTaskDto: ReAssignTaskBody): 40 | Promise { 41 | return this.tasksService.reassignTask(headers, params, completeTaskDto); 42 | } 43 | 44 | 45 | @Get('tasks/stats') 46 | async getStatsByDefinitionId(@Headers() headers: CommonHeadersDto, @Query() query: GetProcessInstanceStatsQueryDto): Promise { 47 | return this.tasksService.getTasksStats(headers, query, false); 48 | } 49 | 50 | @Get('tasks/my-stats') 51 | async getMyStatsByDefinitionId(@Headers() headers: CommonHeadersDto, @Query() query: GetProcessInstanceStatsQueryDto): Promise { 52 | return this.tasksService.getTasksStats(headers, query, true); 53 | } 54 | 55 | 56 | @ApiExcludeEndpoint() 57 | @Get('timer') 58 | async updateTimer(): Promise { 59 | return this.tasksService.updateTimer(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/shared/clients/common-requests.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package auditService; 4 | 5 | message FetchAllRequest { 6 | int32 size = 1; 7 | int32 page = 2; 8 | string search = 3; 9 | string sort = 4; 10 | string filters = 5; 11 | 12 | } 13 | 14 | 15 | message Id { 16 | string id = 1; 17 | } 18 | -------------------------------------------------------------------------------- /src/shared/connectors/grpc.connector.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import GRPCClient from 'src/common/providers/node-grpc-client'; 3 | import { GrpcConnectorDto } from 'src/shared/dtos' 4 | import { Paths } from 'src/common/const/constants'; 5 | 6 | export class GrpcConnector { 7 | 8 | call(grpcConnectorConfig: GrpcConnectorDto) { 9 | 10 | return new Promise((_resolve, reject) => { 11 | try { 12 | const { serviceOptions, methodOptions } = grpcConnectorConfig; 13 | const protoPath = resolve(Paths.WORKFLOW_PROTO_DIR, serviceOptions.protoPath); 14 | const serviceClient = new GRPCClient(protoPath, serviceOptions.packageName || serviceOptions.serviceName, serviceOptions.serviceName, serviceOptions.url); 15 | // options is optional and is supported from version 1.5.0 16 | const options = { 17 | metadata: methodOptions.metadata 18 | }; 19 | serviceClient.runService(methodOptions.methodName, methodOptions.message, (err, res) => { 20 | if (err) { 21 | reject(err); 22 | } else { 23 | _resolve(res); 24 | } 25 | }, options); 26 | } catch (err) { 27 | reject(err); 28 | } 29 | }).then((data) => [null, data]) 30 | .catch((err) => [err]); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/shared/connectors/http.connector.ts: -------------------------------------------------------------------------------- 1 | import { HttpService } from '@nestjs/axios' 2 | import { Injectable } from '@nestjs/common'; 3 | import { AxiosRequestConfig } from 'axios'; 4 | import { firstValueFrom, lastValueFrom, map } from 'rxjs'; 5 | import { HttpConnectorDto } from 'src/shared/dtos' 6 | 7 | @Injectable() 8 | export class HttpConnector { 9 | constructor (private httpService: HttpService) { } 10 | 11 | async call(httpConnectorConfig: HttpConnectorDto) { 12 | const axiosConfig = httpConnectorConfig; 13 | return firstValueFrom(this.httpService.request(axiosConfig)).then((data) => [null, { status: data?.status, data: data?.data }]) 14 | .catch((err) => [{ status: err.response?.status, data: err.response?.data }]); 15 | } 16 | } -------------------------------------------------------------------------------- /src/shared/connectors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './http.connector' 2 | export * from './grpc.connector' 3 | export * from './openAI.connector' -------------------------------------------------------------------------------- /src/shared/connectors/openAI.connector.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { OpenAIConnectorDto } from '../dtos/connectors.dto'; 3 | import { OpenAI } from 'openai'; 4 | 5 | @Injectable() 6 | export class OpenAIConnector { 7 | 8 | async call(openAIConnectorConfig: OpenAIConnectorDto): Promise { 9 | try { 10 | 11 | const openai = new OpenAI({ apiKey: openAIConnectorConfig.apiKey, }); 12 | const prompt = openAIConnectorConfig.prompt; 13 | const responsePromise = openai.chat.completions.create( 14 | { 15 | model: 'gpt-3.5-turbo', 16 | messages: [{ role: "user", content: prompt }], 17 | }, 18 | { 19 | timeout: 10000, 20 | }, 21 | ); 22 | const response = await responsePromise; 23 | return [null, response]; 24 | } catch (error) { 25 | return [error.toString(), null]; 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/shared/dtos/common-headers.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsOptional, IsNumber } from 'class-validator'; 2 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; 3 | 4 | export class CommonHeadersDto { 5 | @IsString() 6 | @ApiPropertyOptional() 7 | @IsOptional() 8 | readonly 'x-tenant-id': string; 9 | 10 | @IsString() 11 | @ApiProperty() 12 | readonly 'authorization': string; 13 | 14 | 'user-id'?: string 15 | } -------------------------------------------------------------------------------- /src/shared/dtos/connectors.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsOptional, IsNotEmpty, IsNotEmptyObject 3 | } from 'class-validator'; 4 | import { AxiosRequestConfig } from 'axios'; 5 | 6 | export class HttpConnectorDto implements AxiosRequestConfig { 7 | url: string; 8 | method: string; 9 | 10 | @IsOptional() 11 | headers: any; 12 | 13 | @IsOptional() 14 | params?: any; 15 | 16 | @IsOptional() 17 | query?: any; 18 | 19 | @IsOptional() 20 | data?: any; 21 | } 22 | 23 | class GrpcServiceOptions { 24 | @IsOptional() 25 | protoPath: string; 26 | 27 | @IsNotEmpty() 28 | serviceName: string; 29 | 30 | @IsNotEmpty() 31 | packageName: string; 32 | 33 | @IsNotEmpty() 34 | url: string; 35 | } 36 | 37 | class GrpcMethodOptions { 38 | @IsNotEmpty() 39 | methodName: string; 40 | 41 | @IsOptional() 42 | message: any; 43 | 44 | @IsOptional() 45 | metadata: any; 46 | } 47 | 48 | export class GrpcConnectorDto { 49 | @IsNotEmptyObject() 50 | serviceOptions: GrpcServiceOptions; 51 | 52 | @IsNotEmptyObject() 53 | methodOptions: GrpcMethodOptions; 54 | } 55 | 56 | export class OpenAIConnectorDto{ 57 | prompt: string; 58 | apiKey: string; 59 | 60 | @IsOptional() 61 | data?: any; 62 | 63 | } -------------------------------------------------------------------------------- /src/shared/dtos/index.ts: -------------------------------------------------------------------------------- 1 | export * from './connectors.dto' 2 | export * from './common-headers.dto' -------------------------------------------------------------------------------- /src/shared/repositories/base/base-repository.impl.ts: -------------------------------------------------------------------------------- 1 | import { ProjectionType, FilterQuery, SortValues, Model, PipelineStage } from 'mongoose'; 2 | import { BaseRepositry } from './base.repository'; 3 | 4 | export abstract class BaseRepositoryImpl implements BaseRepositry{ 5 | model: Model; 6 | 7 | constructor (model: Model) { 8 | this.model = model; 9 | } 10 | 11 | find(where?: FilterQuery, projection?: ProjectionType, 12 | page?: number, limit?: number, sort?: string | { [key: string]: SortValues }): Promise { 13 | let mongoQuery = this.model.find(where, projection); 14 | if (page !== undefined && limit !== undefined) { 15 | mongoQuery = mongoQuery.skip(page * limit).limit(limit); 16 | } 17 | if (sort) { 18 | mongoQuery = mongoQuery.sort(sort); 19 | } 20 | return mongoQuery.exec(); 21 | } 22 | 23 | findOne(where: FilterQuery, projection?: ProjectionType, sort?: string | { [key: string]: SortValues }): Promise { 24 | let mongoQuery = this.model.findOne(where, projection); 25 | if (sort) { 26 | mongoQuery = mongoQuery.sort(sort); 27 | } 28 | return mongoQuery.exec(); 29 | } 30 | 31 | count(where: FilterQuery): Promise { 32 | return this.model.count(where).exec(); 33 | } 34 | 35 | aggregate(pipeline: any[]): Promise { 36 | return this.model.aggregate(pipeline).allowDiskUse(true).exec(); 37 | } 38 | } -------------------------------------------------------------------------------- /src/shared/repositories/base/base.entity.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from '@nestjs/mongoose'; 2 | import { Document } from 'mongoose'; 3 | 4 | export type BaseDocument = Event & Document; 5 | 6 | @Schema({ timestamps: true }) 7 | export class BaseEntity { 8 | 9 | } -------------------------------------------------------------------------------- /src/shared/repositories/base/base.error.ts: -------------------------------------------------------------------------------- 1 | import { HttpException } from '@nestjs/common'; 2 | 3 | export class BaseError extends HttpException { 4 | 5 | constructor(status: number, message: string) { 6 | super(message, status); 7 | } 8 | } -------------------------------------------------------------------------------- /src/shared/repositories/base/base.repository.d.ts: -------------------------------------------------------------------------------- 1 | 2 | import { FilterQuery, ProjectionType, SortValues } from 'mongoose'; 3 | @Injectable() 4 | export interface BaseRepositry { 5 | find(where?: FilterQuery, projection?: ProjectionType, 6 | page?: number, limit?: number, sort?: string | { [key: string]: SortValues }): Promise; 7 | findOne(where: FilterQuery, projection?: ProjectionType): Promise; 8 | 9 | count(where: FilterQuery): Promise; 10 | 11 | aggregate(pipeline: any[]): Promise; 12 | 13 | } -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | }, 9 | "jest": { 10 | "collectCoverage": true, 11 | "collectCoverageFrom": ["./src/**"], 12 | "coverageThreshold": { 13 | "global": { 14 | "lines": 90 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const path = require('path') 3 | const nodeExternals = require('webpack-node-externals') 4 | const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin') 5 | 6 | module.exports = { 7 | entry: ['webpack/hot/poll?100', './src/main.ts'], 8 | target: 'node', 9 | externals: [ 10 | nodeExternals({ 11 | allowlist: ['webpack/hot/poll?100'], 12 | }), 13 | ], 14 | module: { 15 | rules: [ 16 | { 17 | test: /.tsx?$/, 18 | use: 'ts-loader', 19 | exclude: /node_modules/, 20 | }, 21 | ], 22 | }, 23 | mode: 'development', 24 | resolve: { 25 | extensions: ['.tsx', '.ts', '.js'], 26 | }, 27 | plugins: [ 28 | new webpack.HotModuleReplacementPlugin(), 29 | new RunScriptWebpackPlugin({ name: 'server.js' }), 30 | ], 31 | output: { 32 | path: path.join(__dirname, 'dist'), 33 | filename: 'server.js', 34 | }, 35 | } 36 | --------------------------------------------------------------------------------