├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── release-drafter.yml └── workflows │ ├── build-docs.yml │ ├── tag-and-publish.yml │ ├── test-camunda-saas-push.yml │ └── test.yml ├── .gitignore ├── .husky ├── pre-commit └── prepare-commit-msg ├── .npmignore ├── .npmrc ├── .nvmrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEVELOP.md ├── LICENSE ├── README.md ├── bin └── zeebe-node ├── design ├── ZBBatchWorker-state-machine.bpmn ├── grpc-error-handling.md └── grpc-state-machine.bpmn ├── docker ├── application.yaml └── docker-compose.yml ├── docs ├── assets │ ├── css │ │ └── main.css │ ├── images │ │ ├── icons.png │ │ ├── icons@2x.png │ │ ├── widgets.png │ │ └── widgets@2x.png │ └── js │ │ ├── main.js │ │ └── search.js ├── classes │ ├── BpmnParser.html │ ├── ZBClient.html │ ├── ZBLogger.html │ └── ZBWorker.html ├── enums │ ├── PartitionBrokerHealth.html │ └── PartitionBrokerRole.html ├── index.html ├── interfaces │ ├── ActivateInstruction.html │ ├── ActivateJobsRequest.html │ ├── ActivateJobsResponse.html │ ├── ActivatedJob.html │ ├── BasicAuthConfig.html │ ├── BrokerInfo.html │ ├── CamundaCloudConfig.html │ ├── CompleteJobRequest.html │ ├── CreateProcessBaseRequest.html │ ├── CreateProcessInstanceBaseRequest.html │ ├── CreateProcessInstanceReq.html │ ├── CreateProcessInstanceRequest.html │ ├── CreateProcessInstanceResponse.html │ ├── CreateProcessInstanceWithResultReq.html │ ├── CreateProcessInstanceWithResultRequest.html │ ├── CreateProcessInstanceWithResultResponse.html │ ├── DecisionDeployment.html │ ├── DecisionMetadata.html │ ├── DecisionRequirementsDeployment.html │ ├── DecisionRequirementsMetadata.html │ ├── DeployProcessBuffer.html │ ├── DeployProcessRequest.html │ ├── DeployProcessResponse.html │ ├── DeployResourceRequest.html │ ├── DeployResourceResponse.html │ ├── EvaluateDecisionRequest.html │ ├── EvaluateDecisionResponse.html │ ├── EvaluatedDecision.html │ ├── EvaluatedDecisionInput.html │ ├── EvaluatedDecisionOutput.html │ ├── FailJobRequest.html │ ├── ICustomHeaders.html │ ├── IInputVariables.html │ ├── IOutputVariables.html │ ├── IProcessVariables.html │ ├── JSONDoc.html │ ├── Job.html │ ├── JobCompletionInterface.html │ ├── JobFailureConfiguration.html │ ├── KeyedObject.html │ ├── ListProcessResponse.html │ ├── ModifyProcessInstanceRequest.html │ ├── ModifyProcessInstanceResponse.html │ ├── OperationOptionsNoRetry.html │ ├── OperationOptionsWithRetry.html │ ├── Partition.html │ ├── ProcessDeployment.html │ ├── ProcessInstanceCreationStartInstruction.html │ ├── ProcessMetadata.html │ ├── ProcessRequestObject.html │ ├── PublishMessageRequest.html │ ├── PublishMessageResponse.html │ ├── PublishStartMessageRequest.html │ ├── ResolveIncidentRequest.html │ ├── Resource.html │ ├── SetVariablesRequest.html │ ├── TerminateInstruction.html │ ├── ThrowErrorRequest.html │ ├── TopologyResponse.html │ ├── UpdateJobRetriesRequest.html │ ├── VariableInstruction.html │ ├── ZBBatchWorkerConfig.html │ ├── ZBClientOptions.html │ ├── ZBCustomLogger.html │ ├── ZBGrpc.html │ ├── ZBLogMessage.html │ ├── ZBLoggerConfig.html │ ├── ZBLoggerOptions.html │ ├── ZBWorkerBaseConfig.html │ ├── ZBWorkerConfig.html │ ├── ZBWorkerOptions.html │ └── ZeebeJob.html └── modules.html ├── example ├── README.md ├── connector.js ├── connector_test.bpmn ├── process.js ├── start-process-instance.js ├── test.bpmn └── worker.js ├── interfaces └── index.d.ts ├── jest.config.js ├── package-lock.json ├── package.json ├── prettier.config.js ├── proto └── zeebe.proto ├── src ├── .editorconfig ├── __mocks__ │ ├── fs.ts │ └── zbClientOptions.ts ├── __tests__ │ ├── BpmnParser.spec.ts │ ├── ConfigurationHydrator.spec.ts │ ├── OAuthProvider.spec.ts │ ├── StdOut.spec.ts │ ├── ZBClient-unmocked.spec.ts │ ├── ZBClient.spec.ts │ ├── __snapshots__ │ │ └── ZBClient-unmocked.spec.ts.snap │ ├── disconnection │ │ └── disconnect.spec.ts │ ├── integration │ │ ├── Client-BroadcastSignal.spec.ts │ │ ├── Client-BrokenBpmn.spec.ts │ │ ├── Client-ConnectionError.spec.ts │ │ ├── Client-CreateProcessInstanceWithResult.spec.ts │ │ ├── Client-DeployProcess.spec.ts │ │ ├── Client-DeployResource.spec.ts │ │ ├── Client-EvaluateDecision.spec.ts │ │ ├── Client-MessageStart.spec.ts │ │ ├── Client-ModifyProcessInstance.spec.ts │ │ ├── Client-PublishMessage.spec.ts │ │ ├── Client-ThrowError.spec.ts │ │ ├── Client-integration.spec.ts │ │ ├── Client-onReady.spec.ts │ │ ├── Client-setVariables.spec.ts │ │ ├── Client-startProcess.spec.ts │ │ ├── Topology.spec.ts │ │ ├── TypeSurfaceAreaTest.spec.ts │ │ ├── Worker-1.0-complete.spec.ts │ │ ├── Worker-BatchWorker.spec.ts │ │ ├── Worker-Failure-Backoff-Retry.spec.ts │ │ ├── Worker-Failure-Retries.spec.ts │ │ ├── Worker-Failure.spec.ts │ │ ├── Worker-LongPoll.spec.ts │ │ ├── Worker-RaiseIncident.spec.ts │ │ ├── Worker-fetchVariable.spec.ts │ │ ├── Worker-integration.spec.ts │ │ └── Worker-onReady.spec.ts │ ├── local-integration │ │ └── OnConnectionError.spec.ts │ ├── stringifyVariables.spec.ts │ └── testdata │ │ ├── BpmnParser.bpmn │ │ ├── BpmnParser2.bpmn │ │ ├── Client-BrokenBpmn.bpmn │ │ ├── Client-DeployWorkflow.bpmn │ │ ├── Client-MessageStart.bpmn │ │ ├── Client-SkipFirstTask.bpmn │ │ ├── Client-ThrowError.bpmn │ │ ├── Signal.bpmn │ │ ├── Worker-Failure-Retries.bpmn │ │ ├── Worker-Failure1.bpmn │ │ ├── Worker-Failure2.bpmn │ │ ├── Worker-Failure3.bpmn │ │ ├── Worker-LongPoll.bpmn │ │ ├── Worker-RaiseIncident.bpmn │ │ ├── await-outcome-long.bpmn │ │ ├── await-outcome.bpmn │ │ ├── await-outcome.bpmn.zip │ │ ├── conditional-pathway.bpmn │ │ ├── decision.dmn │ │ ├── disconnection.bpmn │ │ ├── form_1.form │ │ ├── generic-test.bpmn │ │ ├── hello-world-complete.bpmn │ │ ├── hello-world.bpmn │ │ ├── modeller7File.bpmn │ │ └── quarantine-duration.dmn ├── index.ts ├── lib │ ├── BpmnParser.ts │ ├── ConfigurationHydrator.ts │ ├── ConnectionFactory.ts │ ├── EnvFunction.ts │ ├── GrpcClient.ts │ ├── GrpcError.ts │ ├── GrpcMiddleware.ts │ ├── JobBatcher.ts │ ├── MockStdOut.ts │ ├── OAuthProvider.ts │ ├── Queue.ts │ ├── SimpleLogger.ts │ ├── StatefulLogInterceptor.ts │ ├── TypedEmitter.ts │ ├── ZBJsonLogger.ts │ ├── ZBLogger.ts │ ├── ZBWorkerBase.ts │ ├── ZBWorkerSignature.ts │ ├── cancelProcesses.ts │ ├── createUniqueTaskType.ts │ ├── deployWorkflow │ │ ├── impure.ts │ │ └── pure.ts │ ├── index.ts │ ├── interfaces-1.0.ts │ ├── interfaces-grpc-1.0.ts │ ├── interfaces-published-contract.ts │ ├── stringifyVariables.ts │ └── utils.ts ├── tsconfig.json └── zb │ ├── ZBBatchWorker.ts │ ├── ZBClient.ts │ └── ZBWorker.ts ├── tsconfig.json └── tslint.json /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Expected Behavior 4 | 5 | 6 | ## Current Behavior 7 | 8 | 9 | ## Possible Solution 10 | 11 | 12 | ## Steps to Reproduce 13 | 14 | 15 | 1. 16 | 2. 17 | 3. 18 | 4. 19 | 20 | ## Context (Environment) 21 | 22 | 23 | 24 | 25 | 26 | ## Detailed Description 27 | 28 | 29 | ## Possible Implementation 30 | 31 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes # 2 | 3 | ## Proposed Changes 4 | 5 | - 6 | - 7 | - 8 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: "v$RESOLVED_VERSION 🌈" 2 | tag-template: "v$RESOLVED_VERSION" 3 | categories: 4 | - title: "🚀 Features" 5 | labels: 6 | - "feature" 7 | - "enhancement" 8 | - title: "🐛 Bug Fixes" 9 | labels: 10 | - "fix" 11 | - "bugfix" 12 | - "bug" 13 | - title: "🧰 Maintenance" 14 | label: "chore" 15 | - title: "Breaking Changes" 16 | label: "breaking" 17 | - title: "Deprecations" 18 | label: "deprecation" 19 | change-template: "- $TITLE @$AUTHOR (#$NUMBER)" 20 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. 21 | version-resolver: 22 | major: 23 | labels: 24 | - "major" 25 | minor: 26 | labels: 27 | - "minor" 28 | patch: 29 | labels: 30 | - "patch" 31 | default: patch 32 | template: | 33 | ## Changes 34 | 35 | $CHANGES 36 | -------------------------------------------------------------------------------- /.github/workflows/build-docs.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish docs 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Version' 8 | required: true 9 | 10 | jobs: 11 | tag-and-publish: 12 | runs-on: ubuntu-latest 13 | services: 14 | elasticsearch: 15 | image: docker.elastic.co/elasticsearch/elasticsearch:7.17.5 16 | env: 17 | discovery.type: single-node 18 | cluster.name: docker-cluster 19 | bootstrap.memory_lock: true 20 | xpack.security.enabled: false 21 | ES_JAVA_OPTS: -Xms1024m -Xmx1024m 22 | ports: 23 | - 9200:9200 24 | - 9300:9300 25 | zeebe: 26 | image: camunda/zeebe:8.3.3 27 | env: 28 | JAVA_TOOL_OPTIONS: "-Xms512m -Xmx512m" 29 | ZEEBE_BROKER_EXPORTERS_ELASTICSEARCH_CLASSNAME: io.camunda.zeebe.exporter.ElasticsearchExporter 30 | ZEEBE_BROKER_EXPORTERS_ELASTICSEARCH_ARGS_URL: http://elasticsearch:9200 31 | ZEEBE_BROKER_EXPORTERS_ELASTICSEARCH_ARGS_BULK_SIZE: 1 32 | ZEEBE_BROKER_BACKPRESSURE_ENABLED: false 33 | ports: 34 | - 26500:26500 35 | steps: 36 | - name: Checkout repository 37 | uses: actions/checkout@v3 38 | with: 39 | fetch-depth: 0 40 | - name: Setup Node 41 | uses: actions/setup-node@v3 42 | with: 43 | node-version: 16.17.0 44 | cache: 'npm' 45 | - name: Install 46 | run: npm ci --ignore-scripts 47 | - name: Run integration tests 48 | run: npm run test:integration 49 | - name: Run tests 50 | run: npm run test 51 | - name: Run local integration tests 52 | run: npm run test:local 53 | - name: Build 54 | run: npm run build 55 | - name: Set version 56 | run: | 57 | git config user.name 'github-actions[bot]' 58 | git config user.email 'github-actions[bot]@users.noreply.github.com' 59 | 60 | - name: Build Docs 61 | run: npm run docs 62 | 63 | - name: Deploy Docs 64 | uses: JamesIves/github-pages-deploy-action@v4 65 | with: 66 | folder: docs 67 | branch: gh-pages 68 | -------------------------------------------------------------------------------- /.github/workflows/tag-and-publish.yml: -------------------------------------------------------------------------------- 1 | name: Tag and publish a new version 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Version' 8 | required: true 9 | 10 | jobs: 11 | tag-and-publish: 12 | runs-on: ubuntu-latest 13 | services: 14 | elasticsearch: 15 | image: docker.elastic.co/elasticsearch/elasticsearch:7.17.5 16 | env: 17 | discovery.type: single-node 18 | cluster.name: docker-cluster 19 | bootstrap.memory_lock: true 20 | xpack.security.enabled: false 21 | ES_JAVA_OPTS: -Xms1024m -Xmx1024m 22 | ports: 23 | - 9200:9200 24 | - 9300:9300 25 | zeebe: 26 | image: camunda/zeebe:8.3.3 27 | env: 28 | JAVA_TOOL_OPTIONS: "-Xms512m -Xmx512m" 29 | ZEEBE_BROKER_EXPORTERS_ELASTICSEARCH_CLASSNAME: io.camunda.zeebe.exporter.ElasticsearchExporter 30 | ZEEBE_BROKER_EXPORTERS_ELASTICSEARCH_ARGS_URL: http://elasticsearch:9200 31 | ZEEBE_BROKER_EXPORTERS_ELASTICSEARCH_ARGS_BULK_SIZE: 1 32 | ZEEBE_BROKER_BACKPRESSURE_ENABLED: false 33 | ports: 34 | - 26500:26500 35 | steps: 36 | - name: Checkout repository 37 | uses: actions/checkout@v3 38 | with: 39 | fetch-depth: 0 40 | - name: Setup Node 41 | uses: actions/setup-node@v3 42 | with: 43 | node-version: 16.17.0 44 | cache: 'npm' 45 | - name: Install 46 | run: npm ci --ignore-scripts 47 | - name: Run integration tests 48 | run: npm run test:integration 49 | - name: Run tests 50 | run: npm run test 51 | - name: Run local integration tests 52 | run: npm run test:local 53 | - name: Build 54 | run: npm run build 55 | - name: Set version 56 | run: | 57 | git config user.name 'github-actions[bot]' 58 | git config user.email 'github-actions[bot]@users.noreply.github.com' 59 | npm version ${{ inputs.version }} -m "Publish v%s" 60 | - name: Publish to NPM 61 | env: 62 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 63 | run: npm publish --access=public 64 | - name: Push changes 65 | run: git push --follow-tags 66 | 67 | - name: Build Docs 68 | run: npm run docs 69 | 70 | - name: Deploy Docs 71 | uses: JamesIves/github-pages-deploy-action@v4 72 | with: 73 | folder: docs 74 | branch: gh-pages 75 | -------------------------------------------------------------------------------- /.github/workflows/test-camunda-saas-push.yml: -------------------------------------------------------------------------------- 1 | name: "Test on Camunda 8 SaaS" 2 | on: # main branch changes 3 | push: 4 | jobs: 5 | test-on-saas: 6 | runs-on: ubuntu-latest 7 | timeout-minutes: 10 8 | steps: 9 | - uses: actions/checkout@v1 10 | - run: | 11 | npm install 12 | npm run build 13 | npm run test:integration 14 | env: 15 | ZEEBE_ADDRESS: ${{ secrets.ZEEBE_ADDRESS }} 16 | ZEEBE_CLIENT_ID: ${{ secrets.ZEEBE_CLIENT_ID }} 17 | ZEEBE_AUTHORIZATION_SERVER_URL: ${{ secrets.ZEEBE_AUTHORIZATION_SERVER_URL }} 18 | ZEEBE_CLIENT_SECRET: ${{ secrets.ZEEBE_CLIENT_SECRET }} 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | push: 4 | jobs: 5 | test: 6 | name: test 7 | runs-on: ubuntu-latest 8 | services: 9 | zeebe: 10 | image: camunda/zeebe:8.3.3 11 | ports: 12 | - 26500:26500 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@master 16 | - name: Set up Node.js 17 | uses: actions/setup-node@master 18 | with: 19 | node-version: 16.17.0 20 | - run: npm install 21 | - run: npm run test:docker 22 | # env: 23 | # ZEEBE_ADDRESS: ${{ secrets.ZEEBE_ADDRESS }} 24 | # ZEEBE_CLIENT_ID: ${{ secrets.ZEEBE_CLIENT_ID }} 25 | # ZEEBE_AUTHORIZATION_SERVER_URL: ${{ secrets.ZEEBE_AUTHORIZATION_SERVER_URL }} 26 | # ZEEBE_CLIENT_SECRET: ${{ secrets.ZEEBE_CLIENT_SECRET }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | .vscode 5 | cctest.ts 6 | t.ts 7 | yolo.bpmn 8 | crash-*.log 9 | camunda-cloud.env 10 | tsconfig.tsbuildinfo 11 | .env 12 | ignore-this 13 | .dccache 14 | target/npmlist.json 15 | *.env -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm test && npm run test:disconnect && npm run-script build && npx --no-install lint-staged 5 | -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | exec < /dev/tty && git cz --hook || true 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | .circleci 3 | guide 4 | src 5 | design 6 | docs 7 | .env 8 | *.env 9 | .vscode 10 | .husky 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" 2 | //registry.npmjs.org/:_authToken=${NPM_TOKEN} 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.17.0 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at code-of-conduct@zeebe.io. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org -------------------------------------------------------------------------------- /DEVELOP.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | _Development conventions._ 4 | 5 | ## Creating a new branch 6 | 7 | Please create a new branch for each feature or fix, and branch from `master`. 8 | 9 | ## Precommit hooks 10 | 11 | There is a precommit hook that runs the disconnection test, which requires Docker to be installed and for no Zeebe container to be running. 12 | 13 | ## Commit messages 14 | 15 | We use [AngularJS's commit message conventions](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines), enforced by [commitizen](https://github.com/commitizen/cz-cli). This is used by [semantic-release](https://www.npmjs.com/package/semantic-release) to automate releases to NPM when PRs are merged into master. 16 | 17 | Run `git commit` without the `-m` flag to get a wizard that will guide you through creating an appropriate commit message. 18 | 19 | ## Pull Requests 20 | 21 | Please [squash and rebase commits on master](https://blog.carbonfive.com/always-squash-and-rebase-your-git-commits/). 22 | 23 | Pull Requests must be labelled before they are merged to master. This is used by [Release Drafter](https://github.com/release-drafter/release-drafter#readme) to automate Release Notes on GitHub. 24 | 25 | ## Publishing a new npm package 26 | 27 | The npm package publishing is handled by a GitHub Workflow using [semantic-release](https://www.npmjs.com/package/semantic-release). 28 | 29 | A new package will be released whenever a PR is merged into master. 30 | 31 | ## Disconnection test 32 | 33 | There is a test that checks that the client can reconnect to a rescheduled broker, or a broker that starts after the client application. 34 | 35 | It can be run with `npm run test:disconnection` and it is run automatically when committing changes. It requires Docker to be running on your machine. 36 | 37 | ## Debug logging 38 | 39 | Try this for the insane level of trace logging: 40 | 41 | ``` 42 | DEBUG=grpc,worker GRPC_TRACE=all GRPC_VERBOSITY=DEBUG jest Worker-LongPoll --detectOpenHandles 43 | ``` 44 | 45 | To log from the Node engine itself (useful in tracking down grpc issues in grpc-js): 46 | 47 | ``` 48 | NODE_DEBUG=http2 GRPC_TRACE=channel,call_stream GRPC_VERBOSITY=DEBUG ZEEBE_NODE_LOGLEVEL=debug ZEEBE_NODE_PUREJS=true npm run test:integration 49 | ``` 50 | 51 | To get extended stack traces: 52 | 53 | ``` 54 | NODE_DEBUG=http2 GRPC_TRACE=channel,call_stream ZEEBE_NODE_PUREJS=true node --expose-internals --expose-gc node_modules/.bin/jest --runInBand --testPathIgnorePatterns disconnection --detectOpenHandles --verbose true Worker-Failure 55 | ``` 56 | 57 | ``` 58 | valgrind node --expose-internals --expose-gc node_modules/.bin/jest --runInBand --testPathIgnorePatterns disconnection --detectOpenHandles Worker-Failure 59 | ``` 60 | 61 | ## Scaffold a Ubuntu machine for dev 62 | 63 | sudo apt install -y build-essential 64 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.2/install.sh | bash 65 | source ~/.bashrc 66 | nvm install 14 67 | git clone https://github.com/camunda-community-hub/zeebe-client-node-js.git 68 | cd zeebe-client-node-js 69 | npm i 70 | 71 | # Set Camunda Cloud env variables 72 | 73 | ZEEBE_NODE_PUREJS=true node_modules/.bin/jest Worker-Failure 74 | -------------------------------------------------------------------------------- /bin/zeebe-node: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const ZB = require('../dist') 3 | 4 | const workflowFile = Array.from(process.argv)[2] 5 | console.log(workflowFile) 6 | ;(async () => { 7 | console.log(await ZB.BpmnParser.scaffold(workflowFile)) 8 | })() 9 | -------------------------------------------------------------------------------- /design/grpc-error-handling.md: -------------------------------------------------------------------------------- 1 | # GRPC Channel Error Handling 2 | 3 | There are a few things that can go wrong on the Grpc channel: 4 | 5 | - **No resolvable DNS address**. In this case, the stream emits an error with `code`: `14`, and `details`: `Name resolution failed for target nobroker:26500`. 6 | - **Resolvable address, but no broker** 7 | - **Broker goes away**. This can be due to a Docker restart, or a K8s pod reschedule (for example, in Camunda Cloud). 8 | - **Intermittent Network Error** 9 | - **Business Error**. For example: 10 | - **Broker backpressure**. This returns error code 8. 11 | -------------------------------------------------------------------------------- /docker/application.yaml: -------------------------------------------------------------------------------- 1 | # Zeebe Standalone Broker configuration file (with embedded gateway) 2 | 3 | # This file is based on broker.standalone.yaml.template but stripped down to contain only a limited 4 | # set of configuration options. These are a good starting point to get to know Zeebe. 5 | # For advanced configuration options, have a look at the templates in this folder. 6 | 7 | # !!! Note that this configuration is not suitable for running a standalone gateway. !!! 8 | # If you want to run a standalone gateway node, please have a look at gateway.yaml.template 9 | 10 | # ---------------------------------------------------- 11 | # Byte sizes 12 | # For buffers and others must be specified as strings and follow the following 13 | # format: "10U" where U (unit) must be replaced with KB = Kilobytes, MB = Megabytes or GB = Gigabytes. 14 | # If unit is omitted then the default unit is simply bytes. 15 | # Example: 16 | # sendBufferSize = "16MB" (creates a buffer of 16 Megabytes) 17 | # 18 | # Time units 19 | # Timeouts, intervals, and the likes, must be specified either in the standard ISO-8601 format used 20 | # by java.time.Duration, or as strings with the following format: "VU", where: 21 | # - V is a numerical value (e.g. 1, 5, 10, etc.) 22 | # - U is the unit, one of: ms = Millis, s = Seconds, m = Minutes, or h = Hours 23 | # 24 | # Paths: 25 | # Relative paths are resolved relative to the installation directory of the broker. 26 | # ---------------------------------------------------- 27 | 28 | zeebe: 29 | broker: 30 | gateway: 31 | # Enable the embedded gateway to start on broker startup. 32 | # This setting can also be overridden using the environment variable ZEEBE_BROKER_GATEWAY_ENABLE. 33 | enable: true 34 | 35 | network: 36 | # Sets the port the embedded gateway binds to. 37 | # This setting can also be overridden using the environment variable ZEEBE_BROKER_GATEWAY_NETWORK_PORT. 38 | port: 26500 39 | 40 | security: 41 | # Enables TLS authentication between clients and the gateway 42 | # This setting can also be overridden using the environment variable ZEEBE_BROKER_GATEWAY_SECURITY_ENABLED. 43 | enabled: false 44 | 45 | network: 46 | # Controls the default host the broker should bind to. Can be overwritten on a 47 | # per binding basis for client, management and replication 48 | # This setting can also be overridden using the environment variable ZEEBE_BROKER_NETWORK_HOST. 49 | host: 0.0.0.0 50 | 51 | data: 52 | # Specify a list of directories in which data is stored. 53 | # This setting can also be overridden using the environment variable ZEEBE_BROKER_DATA_DIRECTORIES. 54 | directories: [ data ] 55 | # The size of data log segment files. 56 | # This setting can also be overridden using the environment variable ZEEBE_BROKER_DATA_LOGSEGMENTSIZE. 57 | logSegmentSize: 512MB 58 | # How often we take snapshots of streams (time unit) 59 | # This setting can also be overridden using the environment variable ZEEBE_BROKER_DATA_SNAPSHOTPERIOD. 60 | snapshotPeriod: 15m 61 | 62 | cluster: 63 | # Specifies the Zeebe cluster size. 64 | # This can also be overridden using the environment variable ZEEBE_BROKER_CLUSTER_CLUSTERSIZE. 65 | clusterSize: 1 66 | # Controls the replication factor, which defines the count of replicas per partition. 67 | # This can also be overridden using the environment variable ZEEBE_BROKER_CLUSTER_REPLICATIONFACTOR. 68 | replicationFactor: 1 69 | # Controls the number of partitions, which should exist in the cluster. 70 | # This can also be overridden using the environment variable ZEEBE_BROKER_CLUSTER_PARTITIONSCOUNT. 71 | partitionsCount: 1 72 | 73 | threads: 74 | # Controls the number of non-blocking CPU threads to be used. 75 | # WARNING: You should never specify a value that is larger than the number of physical cores 76 | # available. Good practice is to leave 1-2 cores for ioThreads and the operating 77 | # system (it has to run somewhere). For example, when running Zeebe on a machine 78 | # which has 4 cores, a good value would be 2. 79 | # This setting can also be overridden using the environment variable ZEEBE_BROKER_THREADS_CPUTHREADCOUNT 80 | cpuThreadCount: 2 81 | # Controls the number of io threads to be used. 82 | # This setting can also be overridden using the environment variable ZEEBE_BROKER_THREADS_IOTHREADCOUNT 83 | ioThreadCount: 2 84 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | services: 4 | zeebe: 5 | restart: always 6 | container_name: zeebe_broker 7 | image: camunda/zeebe:8.3.3 8 | environment: 9 | - ZEEBE_LOG_LEVEL=debug 10 | volumes: 11 | - ./application.yaml:/usr/local/zeebe/config/application.yaml 12 | ports: 13 | - "26500:26500" 14 | - "9600:9600" 15 | -------------------------------------------------------------------------------- /docs/assets/images/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camunda-community-hub/zeebe-client-node-js/7969ce1808c96a87519cb1a3f279287f30637c4b/docs/assets/images/icons.png -------------------------------------------------------------------------------- /docs/assets/images/icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camunda-community-hub/zeebe-client-node-js/7969ce1808c96a87519cb1a3f279287f30637c4b/docs/assets/images/icons@2x.png -------------------------------------------------------------------------------- /docs/assets/images/widgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camunda-community-hub/zeebe-client-node-js/7969ce1808c96a87519cb1a3f279287f30637c4b/docs/assets/images/widgets.png -------------------------------------------------------------------------------- /docs/assets/images/widgets@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camunda-community-hub/zeebe-client-node-js/7969ce1808c96a87519cb1a3f279287f30637c4b/docs/assets/images/widgets@2x.png -------------------------------------------------------------------------------- /docs/interfaces/ICustomHeaders.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ICustomHeaders | zeebe-node 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 28 |
29 |
30 | Options 31 |
32 |
33 | All 34 |
    35 |
  • Public
  • 36 |
  • Public/Protected
  • 37 |
  • All
  • 38 |
39 |
40 | 41 | 42 | 43 | 44 |
45 |
46 | Menu 47 |
48 |
49 |
50 |
51 |
52 |
53 | 61 |

Interface ICustomHeaders

62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |

Hierarchy

70 |
    71 |
  • 72 | ICustomHeaders 73 |
  • 74 |
75 |
76 |
77 |

Indexable

78 |
[key: string]: any
79 |
80 |
81 | 101 |
102 |
103 | 134 |
135 |

Generated using TypeDoc

136 |
137 |
138 | 139 | 140 | -------------------------------------------------------------------------------- /docs/interfaces/KeyedObject.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | KeyedObject | zeebe-node 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 28 |
29 |
30 | Options 31 |
32 |
33 | All 34 |
    35 |
  • Public
  • 36 |
  • Public/Protected
  • 37 |
  • All
  • 38 |
39 |
40 | 41 | 42 | 43 | 44 |
45 |
46 | Menu 47 |
48 |
49 |
50 |
51 |
52 |
53 | 61 |

Interface KeyedObject

62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |

Hierarchy

70 |
    71 |
  • 72 | KeyedObject 73 |
  • 74 |
75 |
76 |
77 |

Indexable

78 |
[key: string]: any
79 |
80 |
81 | 101 |
102 |
103 | 134 |
135 |

Generated using TypeDoc

136 |
137 |
138 | 139 | 140 | -------------------------------------------------------------------------------- /docs/interfaces/ModifyProcessInstanceResponse.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ModifyProcessInstanceResponse | zeebe-node 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 28 |
29 |
30 | Options 31 |
32 |
33 | All 34 |
    35 |
  • Public
  • 36 |
  • Public/Protected
  • 37 |
  • All
  • 38 |
39 |
40 | 41 | 42 | 43 | 44 |
45 |
46 | Menu 47 |
48 |
49 |
50 |
51 |
52 |
53 | 61 |

Interface ModifyProcessInstanceResponse

62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |

Hierarchy

70 |
    71 |
  • 72 | ModifyProcessInstanceResponse 73 |
  • 74 |
75 |
76 |
77 | 97 |
98 |
99 | 130 |
131 |

Generated using TypeDoc

132 |
133 |
134 | 135 | 136 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Example Code 2 | 3 | ## Deploy Example Process 4 | 5 | To deploy the example process: 6 | 7 | ```bash 8 | node process.js 9 | ``` 10 | 11 | ## Start Example Worker 12 | 13 | To start a example worker: 14 | 15 | ```bash 16 | node worker.js 17 | ``` 18 | 19 | ## Create Example Process Instances 20 | 21 | To create example process instances: 22 | 23 | ```bash 24 | node start-process-instance.js 25 | ``` 26 | -------------------------------------------------------------------------------- /example/connector.js: -------------------------------------------------------------------------------- 1 | const { ZBClient } = require('../dist'); 2 | 3 | const zbc = new ZBClient({ 4 | // loglevel: 'DEBUG', 5 | // camundaCloud: { 6 | // clientId: process.env.CLIENT_ID, 7 | // clientSecret: process.env.CLIENT_SECRET, 8 | // clusterId: process.env.CLUSTER_ID, 9 | // clusterRegion: process.env.CLUSTER_REGION, 10 | // }, 11 | }); 12 | 13 | zbc.deployProcess('./connector_test.bpmn').then(res => { 14 | console.log(res); 15 | zbc.createProcessInstance('connectortest1', {}).then(console.log); 16 | }); 17 | 18 | let lastJobReceived = new Date(); 19 | 20 | const zbWorker = zbc.createWorker({ 21 | taskType: 'camunda-cloud-connector', 22 | taskHandler: (job) => { 23 | const now = new Date(); 24 | zbWorker.log( 25 | `received new zb job! Seconds passed since last receive: ${ 26 | (now.valueOf() - lastJobReceived.valueOf()) / 1000 27 | }` 28 | ); 29 | lastJobReceived = new Date(); 30 | return job.complete(); 31 | }, 32 | }); 33 | 34 | zbWorker.on('ready', () => { 35 | zbWorker.log('Zeebe worker is ready!'); 36 | }); 37 | -------------------------------------------------------------------------------- /example/process.js: -------------------------------------------------------------------------------- 1 | // const ZB = require('zeebe-node'); 2 | const ZB = require('../dist') 3 | 4 | ; (async () => { 5 | const zbc = new ZB.ZBClient({ 6 | onConnectionError: () => console.log('Connection Error'), 7 | onReady: () => console.log('Ready to work'), 8 | }) 9 | const topology = await zbc.topology() 10 | console.log(JSON.stringify(topology, null, 2)) 11 | 12 | const res = await zbc.deployProcess('./test.bpmn') 13 | setTimeout(() => console.log(res), 5000) 14 | })() 15 | -------------------------------------------------------------------------------- /example/start-process-instance.js: -------------------------------------------------------------------------------- 1 | // const ZB = require('zeebe-node'); 2 | const ZB = require('../dist') 3 | 4 | const jobs = (async () => { 5 | const zbc = new ZB.ZBClient() 6 | for (let i = 0; i < 10; i++) { 7 | const result = await zbc.createProcessInstance('test-process', { 8 | testData: `process #${i}`, 9 | }) 10 | console.log(result) 11 | } 12 | })() 13 | -------------------------------------------------------------------------------- /example/worker.js: -------------------------------------------------------------------------------- 1 | // const ZB = require('zeebe-node'); 2 | const ZB = require('../dist') 3 | 4 | ;(async () => { 5 | const zbc = new ZB.ZBClient() 6 | const topology = await zbc.topology() 7 | console.log(JSON.stringify(topology, null, 2)) 8 | 9 | await zbc.deployProcess('./test.bpmn') 10 | 11 | zbc.createWorker({ 12 | taskType: 'demo-service', 13 | taskHandler: job => { 14 | console.log(job.variables) 15 | job.complete() 16 | }, 17 | }) // handler) 18 | 19 | setTimeout(() => { 20 | console.log('Closing client...') 21 | zbc.close().then(() => console.log('All workers closed')) 22 | }, 10 * 60 * 1000) 23 | })() 24 | -------------------------------------------------------------------------------- /interfaces/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from '../dist/lib/interfaces-1.0' 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | testPathIgnorePatterns: ['node_modules', 'dist'], 5 | collectCoverageFrom: ['!src/__tests__/lib/cancelProcesses.ts'] 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zeebe-node", 3 | "version": "8.3.2", 4 | "description": "The Node.js client library for the Zeebe Workflow Automation Engine.", 5 | "keywords": [ 6 | "zeebe", 7 | "zeebe.io", 8 | "microservices", 9 | "orchestration", 10 | "bpmn", 11 | "conductor", 12 | "Camunda", 13 | "Netflix", 14 | "cloud", 15 | "automation", 16 | "process", 17 | "workflow", 18 | "Uber", 19 | "Cadence" 20 | ], 21 | "homepage": "https://camunda-community-hub.github.io/zeebe-client-node-js/", 22 | "bugs": { 23 | "url": "https://github.com/camunda-community-hub/zeebe-client-node-js/issues" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/camunda-community-hub/zeebe-client-node-js" 28 | }, 29 | "main": "dist/index.js", 30 | "bin": { 31 | "zeebe-node": "bin/zeebe-node" 32 | }, 33 | "scripts": { 34 | "cm": "cz", 35 | "debug": "NODE_DEBUG=http2 GRPC_TRACE=channel,call_stream GRPC_VERBOSITY=DEBUG ../node/out/Debug/node --inspect-brk node_modules/.bin/jest Worker-Failure", 36 | "docs": "typedoc --tsconfig src/tsconfig.json src/index.ts", 37 | "build": "tsc --build src/tsconfig.json", 38 | "watch": "tsc --build src/tsconfig.json -w", 39 | "watch-docs": "typedoc --watch --tsconfig src/tsconfig.json src/index.ts", 40 | "prepare": "husky install && npm run build", 41 | "test": "jest --detectOpenHandles --testPathIgnorePatterns integration local-integration disconnection", 42 | "test:integration": "jest --runInBand --testPathIgnorePatterns disconnection --detectOpenHandles --verbose true", 43 | "test:local": "jest --runInBand --verbose true --detectOpenHandles local-integration", 44 | "test:docker": "jest --runInBand --testPathIgnorePatterns disconnection local-integration --detectOpenHandles --verbose true", 45 | "test:disconnect": "jest --runInBand --verbose true --detectOpenHandles disconnection", 46 | "test&docs": "npm test && npm run docs", 47 | "dev": "tsc-watch --onSuccess \"npm run test&docs\" -p tsconfig.json --outDir dist" 48 | }, 49 | "lint-staged": { 50 | "*.{md,markdown}": [ 51 | "prettier --write", 52 | "remark", 53 | "git add" 54 | ], 55 | "*.{json,css,html,yaml,yml}": [ 56 | "prettier --write", 57 | "git add" 58 | ], 59 | "*.{ts,tsx,js,jsx}": [ 60 | "prettier --write", 61 | "tslint -c tslint.json --fix", 62 | "git add" 63 | ] 64 | }, 65 | "dependencies": { 66 | "@grpc/grpc-js": "1.9.7", 67 | "@grpc/proto-loader": "0.7.10", 68 | "chalk": "^2.4.2", 69 | "console-stamp": "^3.0.2", 70 | "dayjs": "^1.8.15", 71 | "debug": "^4.2.0", 72 | "fast-xml-parser": "^4.1.3", 73 | "fp-ts": "^2.5.1", 74 | "got": "^11.8.5", 75 | "long": "^4.0.0", 76 | "promise-retry": "^1.1.1", 77 | "stack-trace": "0.0.10", 78 | "typed-duration": "^1.0.12", 79 | "uuid": "^7.0.3" 80 | }, 81 | "devDependencies": { 82 | "@sitapati/testcontainers": "^2.8.1", 83 | "@types/debug": "0.0.31", 84 | "@types/got": "^9.6.9", 85 | "@types/jest": "^27.5.2", 86 | "@types/node": "^14.17.1", 87 | "@types/promise-retry": "^1.1.3", 88 | "@types/stack-trace": "0.0.29", 89 | "@types/uuid": "^3.4.4", 90 | "commitizen": "^4.2.4", 91 | "cz-conventional-changelog": "^3.3.0", 92 | "delay": "^4.3.0", 93 | "husky": "^7.0.0", 94 | "jest": "^27.2.3", 95 | "jest-environment-node-debug": "^2.0.0", 96 | "lint-staged": "^11.0.0", 97 | "operate-api-client": "^1.1.3", 98 | "prettier": "^1.19.1", 99 | "remark": "^13.0.0", 100 | "remark-cli": "^9.0.0", 101 | "remark-lint": "^8.0.0", 102 | "remark-preset-lint-recommended": "^5.0.0", 103 | "ts-jest": "^27.0.5", 104 | "tsc-watch": "^4.4.0", 105 | "tslint": "^6.1.3", 106 | "tslint-config-prettier": "^1.18.0", 107 | "typedoc": "^0.21.10", 108 | "typescript": "^4.4.4" 109 | }, 110 | "author": { 111 | "name": "Josh Wulf", 112 | "email": "josh.wulf@camunda.com" 113 | }, 114 | "contributors": [ 115 | { 116 | "name": "Timothy Colbert" 117 | }, 118 | { 119 | "name": "Jarred Filmer" 120 | }, 121 | { 122 | "name": "Colin Raddatz" 123 | }, 124 | { 125 | "name": "Olivier Albertini" 126 | }, 127 | { 128 | "name": "Patrick Dehn" 129 | } 130 | ], 131 | "engines": { 132 | "node": ">=16.6.1" 133 | }, 134 | "license": "Apache-2.0", 135 | "config": { 136 | "commitizen": { 137 | "path": "./node_modules/cz-conventional-changelog" 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | endOfLine: 'lf', 3 | jsxSingleQuote: true, 4 | overrides: [ 5 | { 6 | files: ['*.yaml', '*.yml'], 7 | options: { 8 | singleQuote: false, 9 | tabWidth: 2, 10 | useTabs: false, 11 | }, 12 | }, 13 | ], 14 | semi: false, 15 | singleQuote: true, 16 | tabWidth: 4, 17 | trailingComma: 'es5', 18 | useTabs: true, 19 | } 20 | -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | 7 | [*.{ts,js,json}] 8 | indent_style = tab 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.{py,md,markdown}] 13 | indent_style = space 14 | indent_size = 4 15 | trim_trailing_whitespace = true 16 | insert_final_newline = true 17 | 18 | [*.{yml,yaml}] 19 | indent_style = space 20 | indent_size = 2 21 | trim_trailing_whitespace = true 22 | insert_final_newline = true 23 | -------------------------------------------------------------------------------- /src/__mocks__/fs.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | readFileSync() { 5 | return 'file-contents' 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /src/__mocks__/zbClientOptions.ts: -------------------------------------------------------------------------------- 1 | import { Loglevel } from '../lib/interfaces-published-contract' 2 | 3 | export const clientOptions = { 4 | _loglevel: 'NONE' as Loglevel, 5 | // it's a setter! 6 | set loglevel(value: Loglevel) { 7 | this._loglevel = value 8 | }, 9 | get loglevel(): Loglevel { 10 | return this._loglevel 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /src/__tests__/BpmnParser.spec.ts: -------------------------------------------------------------------------------- 1 | import { BpmnParser } from '..' 2 | 3 | const testBpmnFile = __dirname + '/testdata/BpmnParser2.bpmn' 4 | const simpleTestBpmnFile = __dirname + '/testdata/BpmnParser.bpmn' 5 | const modeller7File = __dirname + '/testdata/modeller7File.bpmn' 6 | 7 | const parsed = BpmnParser.parseBpmn(testBpmnFile) 8 | const parsedSimple = BpmnParser.parseBpmn(simpleTestBpmnFile) 9 | 10 | test('parses a bpmn file to an Object', () => { 11 | expect(typeof parsed).toBe('object') 12 | expect(typeof parsedSimple).toBe('object') 13 | }) 14 | 15 | test('can parse a file with a message with no name', async () => { 16 | const parsedv7 = await BpmnParser.generateConstantsForBpmnFiles( 17 | modeller7File 18 | ) 19 | // console.log(parsedv7) 20 | expect(typeof parsedv7).toBe('string') 21 | }) 22 | 23 | test('gets a unique list of task types when passed an object', async () => { 24 | const taskTypes = await BpmnParser.getTaskTypes(parsed) 25 | expect(taskTypes.length).toBe(2) 26 | }) 27 | 28 | test('gets a list of unique task types when passed an array', async () => { 29 | const taskTypes = await BpmnParser.getTaskTypes([parsed, parsedSimple]) 30 | expect(taskTypes.length).toBe(3) 31 | }) 32 | 33 | test('gets a list of unique message names when passed an object', async () => { 34 | const messageNames = await BpmnParser.getMessageNames(parsed) 35 | expect(messageNames.length).toBe(2) 36 | }) 37 | 38 | test('gets a list of unique message names when passed an array', async () => { 39 | const messageNames = await BpmnParser.getMessageNames([ 40 | parsed, 41 | parsedSimple, 42 | ]) 43 | expect(messageNames.length).toBe(3) 44 | }) 45 | 46 | test('Returns a constants file for a single Bpmn file', async () => { 47 | const constants = await BpmnParser.generateConstantsForBpmnFiles( 48 | testBpmnFile 49 | ) 50 | expect(constants.indexOf('console-log')).not.toBe(-1) 51 | }) 52 | 53 | test('Returns a constants file for an array of Bpmn files', async () => { 54 | const constants = await BpmnParser.generateConstantsForBpmnFiles([ 55 | testBpmnFile, 56 | simpleTestBpmnFile, 57 | ]) 58 | expect( 59 | constants 60 | .split(' ') 61 | .join('') 62 | .split('\n') 63 | .join('') 64 | .indexOf( 65 | `exportenumMessageName{MSG_EMIT_FRAME="MSG-EMIT_FRAME",MSG_EMIT_FRAME_1="MSG-EMIT_FRAME` 66 | ) 67 | ).not.toBe(-1) 68 | }) 69 | -------------------------------------------------------------------------------- /src/__tests__/StdOut.spec.ts: -------------------------------------------------------------------------------- 1 | import { ZBClient } from '..' 2 | import { MockStdOut } from '../lib/MockStdOut' 3 | 4 | jest.setTimeout(15000) 5 | 6 | test('uses an injected stdout', done => { 7 | const mockStd = new MockStdOut() 8 | const z = new ZBClient({ stdout: mockStd, eagerConnection: false }) 9 | 10 | // tslint:disable-next-line: no-console 11 | z.createWorker({ 12 | taskType: 'test', 13 | taskHandler: job => { 14 | return job.complete() 15 | }, 16 | }) 17 | setTimeout(() => { 18 | z.close() 19 | }, 2000) 20 | setTimeout(() => { 21 | expect(mockStd.messages.length > 0).toBe(true) 22 | done() 23 | }, 4000) 24 | }) 25 | -------------------------------------------------------------------------------- /src/__tests__/ZBClient-unmocked.spec.ts: -------------------------------------------------------------------------------- 1 | import { ZBClient } from '..' 2 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 3 | 4 | jest.setTimeout(13000) 5 | 6 | test('ZBClient constructor throws an exception when there is no broker and retry is false', async () => { 7 | const zbc = new ZBClient('localhoster', { retry: false }) 8 | expect.assertions(1) 9 | try { 10 | await zbc.deployProcess('./src/__tests__/testdata/hello-world.bpmn') 11 | } catch (e: any) { 12 | expect(e.message.indexOf('14 UNAVAILABLE:')).toEqual(0) 13 | } 14 | await zbc.close() 15 | }) 16 | 17 | test('cancelProcessInstance throws an exception when workflowInstanceKey is malformed', async () => { 18 | const zbc = new ZBClient('localhoster', { retry: false }) 19 | expect.assertions(1) 20 | try { 21 | await zbc.cancelProcessInstance('hello-world') 22 | } catch (e: any) { 23 | expect(e).toMatchSnapshot() 24 | } 25 | await zbc.close() 26 | }) 27 | -------------------------------------------------------------------------------- /src/__tests__/ZBClient.spec.ts: -------------------------------------------------------------------------------- 1 | import { Loglevel, ZBClient } from '..' 2 | const clientOptions = { 3 | loglevel: 'NONE' as Loglevel, 4 | } 5 | process.env.ZEEBE_NODE_LOG_LEVEL = 'NONE' 6 | const previousLogLevelEnv = process.env.ZEEBE_NODE_LOG_LEVEL 7 | 8 | beforeEach(() => { 9 | process.env.ZEEBE_NODE_LOG_LEVEL = 10 | process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 11 | }) 12 | afterEach(() => { 13 | process.env.ZEEBE_NODE_LOG_LEVEL = previousLogLevelEnv 14 | }) 15 | test('ZBClient constructor creates a new ZBClient', () => { 16 | const zbc = new ZBClient() 17 | expect(zbc instanceof ZBClient).toBe(true) 18 | zbc.close() 19 | }) 20 | test('ZBClient constructor appends the port number 26500 to the gatewayAddress by default', () => { 21 | const zbc = new ZBClient('localhost') 22 | expect(zbc.gatewayAddress).toBe('localhost:26500') 23 | zbc.close() 24 | }) 25 | test('ZBClient constructor accepts a custom port number for the gatewayAddress', () => { 26 | const zbc = new ZBClient('localhost:123') 27 | expect(zbc.gatewayAddress).toBe('localhost:123') 28 | zbc.close() 29 | }) 30 | test('ZBClient constructor takes client options passed in Ctor when ZEEBE_NODE_LOG_LEVEL is not defined', () => { 31 | process.env.ZEEBE_NODE_LOG_LEVEL = '' 32 | clientOptions.loglevel = 'NONE' 33 | const z = new ZBClient(clientOptions) 34 | expect(z.loglevel).toBe('NONE') 35 | z.close() 36 | }) 37 | test('ZEEBE_NODE_LOG_LEVEL precedes options passed in Ctor', () => { 38 | process.env.ZEEBE_NODE_LOG_LEVEL = 'NONE' 39 | clientOptions.loglevel = 'DEBUG' 40 | const z = new ZBClient(clientOptions) 41 | expect(z.loglevel).toBe('NONE') 42 | z.close() 43 | }) 44 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/ZBClient-unmocked.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`cancelProcessInstance throws an exception when workflowInstanceKey is malformed 1`] = ` 4 | [Error: 5 | processInstanceKey is malformed, value : hello-world 6 | ] 7 | `; 8 | -------------------------------------------------------------------------------- /src/__tests__/disconnection/disconnect.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable-next-line: no-implicit-dependencies 2 | import { GenericContainer, Wait } from '@sitapati/testcontainers' 3 | import { ZBClient } from '../..' 4 | 5 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 6 | 7 | const ZEEBE_DOCKER_TAG = '8.0.2' 8 | 9 | jest.setTimeout(900000) 10 | 11 | let container 12 | let worker 13 | 14 | afterEach(async () => { 15 | await container?.stop() 16 | await worker?.close() 17 | }) 18 | 19 | function log(msg) { 20 | // tslint:disable-next-line: no-console 21 | console.log(new Date().toString(), msg) // @DEBUG 22 | } 23 | 24 | test('reconnects after a pod reschedule', () => 25 | new Promise(async resolve => { 26 | let readyCount = 0 27 | let errorCount = 0 28 | const delay = timeout => 29 | new Promise(res => setTimeout(() => res(null), timeout)) 30 | 31 | // tslint:disable-next-line: no-console 32 | log('##### Starting container (reconnects after a pod reschedule)') // @DEBUG 33 | 34 | container = await new GenericContainer( 35 | 'camunda/zeebe', 36 | ZEEBE_DOCKER_TAG, 37 | undefined, 38 | 26500 39 | ) 40 | .withExposedPorts(26500) 41 | .withWaitStrategy(Wait.forLogMessage('Broker is ready!')) 42 | .start() 43 | 44 | await delay(10000) 45 | 46 | const zbc = new ZBClient(`localhost`) 47 | // tslint:disable-next-line: no-console 48 | log('##### Deploying workflow') // @DEBUG 49 | 50 | await zbc.deployProcess('./src/__tests__/testdata/disconnection.bpmn') 51 | worker = zbc 52 | .createWorker({ 53 | longPoll: 10000, 54 | pollInterval: 300, 55 | taskHandler: job => { 56 | // tslint:disable-next-line: no-console 57 | log('##### Executing task handler') // @DEBUG 58 | 59 | return job.complete() 60 | }, 61 | taskType: 'disconnection-task', 62 | }) 63 | .on('connectionError', () => { 64 | errorCount++ 65 | }) 66 | .on('ready', () => { 67 | readyCount++ 68 | }) 69 | 70 | // tslint:disable-next-line: no-console 71 | log('##### Starting workflow') // @DEBUG 72 | 73 | const wf = await zbc.createProcessInstanceWithResult({ 74 | bpmnProcessId: 'disconnection', 75 | requestTimeout: 25000, 76 | variables: {}, 77 | }) 78 | expect(wf.bpmnProcessId).toBeTruthy() 79 | 80 | // tslint:disable-next-line: no-console 81 | log('##### Workflow finished') // @DEBUG 82 | 83 | // tslint:disable-next-line: no-console 84 | log('##### Stopping container...') // @DEBUG 85 | 86 | await container.stop() 87 | 88 | // tslint:disable-next-line: no-console 89 | log('##### Container stopped.') // @DEBUG 90 | 91 | // tslint:disable-next-line: no-console 92 | log('##### Starting container....') // @DEBUG 93 | 94 | container = await new GenericContainer( 95 | 'camunda/zeebe', 96 | ZEEBE_DOCKER_TAG, 97 | undefined, 98 | 26500 99 | ) 100 | .withExposedPorts(26500) 101 | .withEnv('ZEEBE_LOG_LEVEL', 'trace') 102 | .withWaitStrategy(Wait.forLogMessage('Broker is ready!')) 103 | .start() 104 | 105 | // tslint:disable-next-line: no-console 106 | log('##### Container started.') // @DEBUG 107 | 108 | await delay(10000) 109 | 110 | // tslint:disable-next-line: no-console 111 | log('##### Deploying workflow 2') // @DEBUG 112 | await zbc.deployProcess('./src/__tests__/testdata/disconnection.bpmn') 113 | 114 | // tslint:disable-next-line: no-console 115 | // console.log('Workflow 2 deployed', _) // @DEBUG 116 | 117 | await delay(15000) 118 | 119 | // tslint:disable-next-line: no-console 120 | log('##### Starting workflow 2') // @DEBUG 121 | 122 | const wf1 = await zbc.createProcessInstanceWithResult( 123 | 'disconnection', 124 | {} 125 | ) 126 | expect(wf1.bpmnProcessId).toBeTruthy() 127 | await worker.close() 128 | await container.stop() 129 | container = undefined 130 | worker = undefined 131 | expect(readyCount).toBe(2) 132 | expect(errorCount).toBe(2) // Had to increment to 2 for the pure JS client. Investigate this later. 133 | resolve(null) 134 | })) 135 | 136 | test('a worker that started first, connects to a broker that starts later', () => 137 | new Promise(async resolve => { 138 | let readyCount = 0 139 | let errorCount = 0 140 | 141 | const delay = timeout => 142 | new Promise(res => setTimeout(() => res(null), timeout)) 143 | 144 | const zbc = new ZBClient(`localhost`) 145 | worker = zbc 146 | .createWorker({ 147 | taskHandler: job => job.complete(), 148 | taskType: 'disconnection-task', 149 | }) 150 | .on('connectionError', () => { 151 | errorCount++ 152 | }) 153 | .on('ready', () => { 154 | readyCount++ 155 | }) 156 | 157 | container = await new GenericContainer( 158 | 'camunda/zeebe', 159 | ZEEBE_DOCKER_TAG, 160 | undefined, 161 | 26500 162 | ) 163 | .withExposedPorts(26500) 164 | .withWaitStrategy(Wait.forLogMessage('Broker is ready!')) 165 | .start() 166 | 167 | await delay(10000) 168 | 169 | await zbc.deployProcess('./src/__tests__/testdata/disconnection.bpmn') 170 | await delay(1000) // Ensure deployment has happened 171 | const wf = await zbc.createProcessInstanceWithResult( 172 | 'disconnection', 173 | {} 174 | ) 175 | expect(wf.bpmnProcessId).toBeTruthy() 176 | await worker.close() 177 | await container.stop() 178 | container = undefined 179 | worker = undefined 180 | expect(readyCount).toBe(1) 181 | expect(errorCount).toBe(1) 182 | resolve(null) 183 | })) 184 | -------------------------------------------------------------------------------- /src/__tests__/integration/Client-BroadcastSignal.spec.ts: -------------------------------------------------------------------------------- 1 | import { cancelProcesses } from '../../lib/cancelProcesses' 2 | import { ZBClient } from '../..' 3 | import { CreateProcessInstanceResponse } from '../../lib/interfaces-grpc-1.0' 4 | 5 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 6 | jest.setTimeout(60000) 7 | 8 | 9 | const zbc = new ZBClient() 10 | let pid: string 11 | let wf: CreateProcessInstanceResponse | undefined 12 | 13 | beforeAll(async () => { 14 | const res = await zbc.deployResource({ 15 | processFilename: `./src/__tests__/testdata/Signal.bpmn` 16 | }) 17 | pid = res.deployments[0].process.bpmnProcessId 18 | await cancelProcesses(pid) 19 | }) 20 | 21 | afterEach(async () => { 22 | try { 23 | if (wf?.processInstanceKey) { 24 | await zbc.cancelProcessInstance(wf.processInstanceKey) 25 | } 26 | } catch (e: any) { 27 | // console.log('Caught NOT FOUND') // @DEBUG 28 | } 29 | }) 30 | 31 | afterAll(async () => { 32 | await zbc.close() 33 | await cancelProcesses(pid) 34 | }) 35 | 36 | test('Can start a process with a signal', () => new Promise(async resolve => { 37 | zbc.createWorker({ 38 | taskType: 'signal-service-task', 39 | taskHandler: job => { 40 | const ack = job.complete() 41 | expect (job.variables.success).toBe(true) 42 | resolve(null) 43 | return ack 44 | } 45 | }) 46 | await zbc.deployResource({ 47 | processFilename: `./src/__tests__/testdata/Signal.bpmn` 48 | }) 49 | const res = await zbc.broadcastSignal({ 50 | signalName: 'test-signal', 51 | variables: { 52 | success: true 53 | } 54 | }) 55 | expect(res.key).toBeTruthy() 56 | }) 57 | ) 58 | 59 | -------------------------------------------------------------------------------- /src/__tests__/integration/Client-BrokenBpmn.spec.ts: -------------------------------------------------------------------------------- 1 | import { ZBClient } from '../..' 2 | 3 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 4 | 5 | const zbc = new ZBClient() 6 | 7 | afterAll(async () => { 8 | await zbc.close() // Makes sure we don't forget to close connection 9 | }) 10 | 11 | test('does not retry the deployment of a broken BPMN file', async () => { 12 | expect.assertions(1) 13 | try { 14 | await zbc.deployProcess( 15 | './src/__tests__/testdata/Client-BrokenBpmn.bpmn' 16 | ) 17 | } catch (e: any) { 18 | expect(e.message.indexOf('3 INVALID_ARGUMENT:')).toBe(0) 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /src/__tests__/integration/Client-ConnectionError.spec.ts: -------------------------------------------------------------------------------- 1 | import { ZBClient } from '../..' 2 | 3 | jest.setTimeout(10000) 4 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 5 | 6 | test(`Calls the onConnectionError handler if there is no broker and eagerConnection: true`, () => 7 | new Promise(async done => { 8 | let called = false 9 | const zbc2 = new ZBClient('localtoast: 267890', { 10 | eagerConnection: true, 11 | onConnectionError: () => { 12 | called = true 13 | }, 14 | }) // Broker doesn't exist!!! 15 | setTimeout(async () => { 16 | expect(called).toBe(true) 17 | await zbc2.close() 18 | done(null) 19 | }, 7000) 20 | })) 21 | 22 | test(`Sets connected:false if there is no broker and no setting of eagerConnection`, () => 23 | new Promise(async done => { 24 | const zbc2 = new ZBClient('localtoast: 267890') // Broker doesn't exist!!! 25 | setTimeout(async () => { 26 | expect(zbc2.connected).toBe(false) 27 | await zbc2.close() 28 | done(null) 29 | }, 5000) 30 | })) 31 | 32 | test(`Sets connected:false if there is no broker and eagerConnection: true`, done => { 33 | const zbc2 = new ZBClient('localtoast: 267890', { 34 | eagerConnection: true, 35 | }) // Broker doesn't exist!!! 36 | setTimeout(async () => { 37 | expect(zbc2.connected).toBe(false) 38 | await zbc2.close() 39 | done() 40 | }, 5000) 41 | }) 42 | 43 | test(`Does emit the connectionError event if there is no broker and eagerConnection: true`, done => { 44 | let called = 0 45 | const zbc2 = new ZBClient('localtoast: 267890', { 46 | eagerConnection: true, 47 | }).on('connectionError', () => { 48 | called++ 49 | }) 50 | 51 | setTimeout(async () => { 52 | expect(called).toBe(1) 53 | await zbc2.close() 54 | done() 55 | }, 7000) 56 | }) 57 | -------------------------------------------------------------------------------- /src/__tests__/integration/Client-CreateProcessInstanceWithResult.spec.ts: -------------------------------------------------------------------------------- 1 | import { cancelProcesses } from '../../lib/cancelProcesses' 2 | import { DeployProcessResponse, ZBClient } from '../..' 3 | 4 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 5 | jest.setTimeout(25000) 6 | 7 | const zbc = new ZBClient() 8 | let test1: DeployProcessResponse 9 | let test2: DeployProcessResponse 10 | let test3: DeployProcessResponse 11 | 12 | beforeAll(async () => { 13 | test1 = await zbc.deployProcess('./src/__tests__/testdata/await-outcome.bpmn') 14 | test2 = await zbc.deployProcess('./src/__tests__/testdata/await-outcome-long.bpmn') 15 | test3 = await zbc.deployProcess('./src/__tests__/testdata/await-outcome.bpmn') 16 | await cancelProcesses(test1.processes[0].bpmnProcessId) 17 | await cancelProcesses(test2.processes[0].bpmnProcessId) 18 | await cancelProcesses(test3.processes[0].bpmnProcessId) 19 | }) 20 | 21 | afterAll(async () => { 22 | await zbc.close() // Makes sure we don't forget to close connection 23 | await cancelProcesses(test1.processes[0].bpmnProcessId) 24 | await cancelProcesses(test2.processes[0].bpmnProcessId) 25 | await cancelProcesses(test3.processes[0].bpmnProcessId) 26 | }) 27 | 28 | test('Awaits a process outcome', async () => { 29 | const processId = test1.processes[0].bpmnProcessId 30 | const result = await zbc.createProcessInstanceWithResult({ 31 | bpmnProcessId: processId, 32 | variables: { 33 | sourceValue: 5 34 | } 35 | }) 36 | expect(result.variables.sourceValue).toBe(5) 37 | }) 38 | 39 | test('can override the gateway timeout', async () => { 40 | const processId = test2.processes[0].bpmnProcessId 41 | const result = await zbc.createProcessInstanceWithResult({ 42 | bpmnProcessId: processId, 43 | requestTimeout: 25000, 44 | variables: { 45 | otherValue: 'rome', 46 | sourceValue: 5, 47 | } 48 | }) 49 | expect(result.variables.sourceValue).toBe(5) 50 | }) 51 | 52 | test('fetches a subset of variables', async () => { 53 | const processId = test3.processes[0].bpmnProcessId 54 | const result = await zbc.createProcessInstanceWithResult({ 55 | bpmnProcessId: processId, 56 | fetchVariables: ['otherValue'], 57 | variables: { 58 | otherValue: 'rome', 59 | sourceValue: 5, 60 | }, 61 | }) 62 | expect(result.variables.sourceValue).toBe(undefined) 63 | expect(result.variables.otherValue).toBe('rome') 64 | }) 65 | -------------------------------------------------------------------------------- /src/__tests__/integration/Client-DeployProcess.spec.ts: -------------------------------------------------------------------------------- 1 | import { ZBClient } from '../../index' 2 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 3 | jest.setTimeout(20000) 4 | 5 | test('deploys a process', async () => { 6 | const zbc = new ZBClient() 7 | const result = await zbc.deployProcess(`./src/__tests__/testdata/Client-DeployWorkflow.bpmn`) 8 | await zbc.close() 9 | expect(result.processes[0].bpmnProcessId).toBeTruthy() 10 | }) 11 | -------------------------------------------------------------------------------- /src/__tests__/integration/Client-DeployResource.spec.ts: -------------------------------------------------------------------------------- 1 | import { cancelProcesses } from '../../lib/cancelProcesses' 2 | import { ZBClient, BpmnParser } from '../../index' 3 | import fs from 'fs' 4 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 5 | jest.setTimeout(20000) 6 | 7 | const zbc = new ZBClient() 8 | const bpmnString = fs.readFileSync(`./src/__tests__/testdata/Client-DeployWorkflow.bpmn`, 'utf8') 9 | const expectedPid = BpmnParser.getProcessId(bpmnString) 10 | 11 | beforeAll(async () => 12 | await cancelProcesses(expectedPid) 13 | ) 14 | 15 | afterAll(async () => 16 | await zbc.close() 17 | ) 18 | 19 | test('deploys a process', async () => { 20 | const result = await zbc.deployResource({ 21 | process: Buffer.from(bpmnString), 22 | name: `Client-DeployWorkflow.bpmn`, 23 | }) 24 | expect(result.deployments[0].process.bpmnProcessId).toBe(expectedPid) 25 | }) 26 | test('deploys a process from a file', async () => { 27 | const result = await zbc.deployResource({ 28 | processFilename: `./src/__tests__/testdata/Client-DeployWorkflow.bpmn`, 29 | }) 30 | expect(result.deployments[0].process.version).toBeGreaterThanOrEqual(1) 31 | }) 32 | test('deploys a DMN table from a filename', async () => { 33 | const result = await zbc.deployResource({ 34 | decisionFilename: './src/__tests__/testdata/quarantine-duration.dmn', 35 | }) 36 | expect(result.deployments[0].decision.decisionKey).not.toBeNull() 37 | }) 38 | test('deploys a DMN table', async () => { 39 | const decision = fs.readFileSync( 40 | './src/__tests__/testdata/quarantine-duration.dmn' 41 | ) 42 | const result = await zbc.deployResource({ 43 | decision, 44 | name: 'quarantine-duration.dmn', 45 | }) 46 | expect(result.deployments[0].decision.decisionKey).not.toBeNull() 47 | }) 48 | test('deploys a Form', async () => { 49 | const form = fs.readFileSync( 50 | './src/__tests__/testdata/form_1.form' 51 | ) 52 | const result = await zbc.deployResource({ 53 | form, 54 | name: 'form_1.form', 55 | }) 56 | expect(result.deployments[0].form).not.toBeNull() 57 | }) 58 | -------------------------------------------------------------------------------- /src/__tests__/integration/Client-EvaluateDecision.spec.ts: -------------------------------------------------------------------------------- 1 | import { ZBClient } from '../../index' 2 | 3 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 4 | 5 | test("EvaluateDecision", async () => { 6 | const zbc = new ZBClient() 7 | const res = await zbc.deployResource({ 8 | decisionFilename: `./src/__tests__/testdata/decision.dmn` 9 | }) 10 | 11 | const dmnDecisionName = "My Decision" 12 | expect(res.deployments[0].decision.dmnDecisionName).toBe(dmnDecisionName) 13 | 14 | const dmnDecisionId = "Decision_13dmfgp" 15 | const r = await zbc.evaluateDecision({ 16 | decisionId: dmnDecisionId, 17 | variables: {season: "fall"} 18 | }) 19 | expect(r.evaluatedDecisions.length).toBe(1) 20 | 21 | await zbc.close() 22 | }) 23 | /** 24 | { 25 | "evaluatedDecisions": [ 26 | { 27 | "matchedRules": [], 28 | "evaluatedInputs": [ 29 | { 30 | "inputId": "Input_1", 31 | "inputName": "season", 32 | "inputValue": "\"fall\"" 33 | } 34 | ], 35 | "decisionKey": "2251799813848760", 36 | "decisionId": "Decision_13dmfgp", 37 | "decisionName": "My Decision", 38 | "decisionVersion": 1, 39 | "decisionType": "DECISION_TABLE", 40 | "decisionOutput": "null" 41 | } 42 | ], 43 | "decisionKey": "2251799813848760", 44 | "decisionId": "Decision_13dmfgp", 45 | "decisionName": "My Decision", 46 | "decisionVersion": 1, 47 | "decisionRequirementsId": "Definitions_1j6sjj9", 48 | "decisionRequirementsKey": "2251799813848759", 49 | "decisionOutput": "null", 50 | "failedDecisionId": "", 51 | "failureMessage": "" 52 | } 53 | */ 54 | -------------------------------------------------------------------------------- /src/__tests__/integration/Client-MessageStart.spec.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuid } from 'uuid' 2 | import { DeployProcessResponse, ZBClient } from '../..' 3 | import { cancelProcesses } from '../../lib/cancelProcesses' 4 | 5 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 6 | jest.setTimeout(45000) 7 | let test1: DeployProcessResponse 8 | const zbc = new ZBClient() 9 | 10 | beforeAll(async () => { 11 | test1 = await zbc.deployProcess('./src/__tests__/testdata/Client-MessageStart.bpmn') 12 | await cancelProcesses(test1.processes[0].bpmnProcessId) 13 | }) 14 | 15 | afterAll(async () => { 16 | await zbc.close() 17 | await cancelProcesses(test1.processes[0].bpmnProcessId) 18 | }) 19 | 20 | test('Can start a process with a message', () => 21 | new Promise(async done => { 22 | 23 | const randomId = uuid() 24 | 25 | // Wait 1 second to make sure the deployment is complete 26 | await new Promise(res => setTimeout(() => res(null), 1000)) 27 | 28 | await zbc.publishStartMessage({ 29 | name:'MSG-START_JOB', 30 | timeToLive: 2000, 31 | variables: { 32 | testKey: randomId, 33 | }, 34 | }) 35 | 36 | zbc.createWorker({ 37 | taskType: 'console-log-msg-start', 38 | taskHandler: async job => { 39 | const res = await job.complete() 40 | expect(job.variables.testKey).toBe(randomId) // Makes sure the worker isn't responding to another message 41 | done(null) 42 | return res 43 | }, 44 | loglevel: 'NONE', 45 | }) 46 | })) 47 | -------------------------------------------------------------------------------- /src/__tests__/integration/Client-ModifyProcessInstance.spec.ts: -------------------------------------------------------------------------------- 1 | import { cancelProcesses } from "../../lib/cancelProcesses"; 2 | import { ZBClient } from "../../index"; 3 | 4 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 5 | 6 | const zbc = new ZBClient() 7 | let pid: string 8 | let processModelId: string 9 | 10 | beforeAll(async () => { 11 | const res = await zbc.deployProcess('./src/__tests__/testdata/Client-SkipFirstTask.bpmn') 12 | processModelId = res.processes[0].bpmnProcessId 13 | }) 14 | afterAll(async () => { 15 | zbc.cancelProcessInstance(pid).catch(_ => _) 16 | await zbc.close() 17 | await cancelProcesses(processModelId) 18 | }) 19 | 20 | test('Modify Process Instance', done =>{ 21 | zbc.deployProcess('./src/__tests__/testdata/Client-SkipFirstTask.bpmn') 22 | zbc.createWorker({ 23 | taskType: 'second_service_task', 24 | taskHandler: job => { 25 | expect(job.variables.second).toBe(1) 26 | return job.complete().then(() => done()) 27 | } 28 | }) 29 | zbc.createProcessInstance({ 30 | bpmnProcessId: 'SkipFirstTask', 31 | variables: {} 32 | }).then(res => { 33 | pid = res.processInstanceKey 34 | zbc.modifyProcessInstance({ 35 | processInstanceKey: res.processInstanceKey, 36 | activateInstructions: [{ 37 | elementId: 'second_service_task', 38 | ancestorElementInstanceKey: "-1", 39 | variableInstructions: [{ 40 | scopeId: '', 41 | variables: { second: 1} 42 | }] 43 | }] 44 | }) 45 | }) 46 | }) 47 | 48 | -------------------------------------------------------------------------------- /src/__tests__/integration/Client-PublishMessage.spec.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuid } from 'uuid' 2 | import { DeployProcessResponse, ZBClient } from '../..' 3 | import { cancelProcesses } from '../../lib/cancelProcesses' 4 | 5 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 6 | jest.setTimeout(45000) 7 | const zbc = new ZBClient() 8 | let deploy: DeployProcessResponse 9 | 10 | beforeAll(async () => { 11 | deploy = await zbc.deployProcess('./src/__tests__/testdata/Client-MessageStart.bpmn') 12 | }) 13 | 14 | afterAll(async () => { 15 | await zbc.close() 16 | await cancelProcesses(deploy.processes[0].bpmnProcessId) 17 | }) 18 | 19 | test('Can publish a message', () => 20 | new Promise(async done => { 21 | const randomId = uuid() 22 | 23 | // Wait 1 second to make sure the deployment is complete 24 | await new Promise(res => setTimeout(() => res(null), 1000)) 25 | 26 | await zbc.publishMessage({ 27 | name: 'MSG-START_JOB', 28 | variables: { 29 | testKey: randomId, 30 | }, 31 | correlationKey: 'something' 32 | }) 33 | 34 | zbc.createWorker({ 35 | taskType: 'console-log-msg-start', 36 | taskHandler: async job => { 37 | const res = await job.complete() 38 | expect(job.variables.testKey).toBe(randomId) // Makes sure the worker isn't responding to another message 39 | done(null) 40 | return res 41 | }, 42 | loglevel: 'NONE', 43 | }) 44 | })) 45 | -------------------------------------------------------------------------------- /src/__tests__/integration/Client-ThrowError.spec.ts: -------------------------------------------------------------------------------- 1 | import { Duration } from 'typed-duration' 2 | import { ZBClient } from '../..' 3 | import { cancelProcesses } from '../../lib/cancelProcesses' 4 | 5 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 6 | jest.setTimeout(25000) 7 | 8 | let processId: string 9 | 10 | let zbc: ZBClient 11 | 12 | beforeAll(async () => { 13 | const zb = new ZBClient() 14 | processId = (await zb.deployProcess('./src/__tests__/testdata/Client-ThrowError.bpmn')).processes[0].bpmnProcessId 15 | cancelProcesses(processId) 16 | await zb.close() 17 | }) 18 | 19 | beforeEach(() => { 20 | zbc = new ZBClient() 21 | }) 22 | 23 | afterEach(async () => { 24 | await zbc.close() 25 | }) 26 | 27 | afterAll(async () => { 28 | cancelProcesses(processId) 29 | }) 30 | 31 | test('Throws a business error that is caught in the process', async () => { 32 | zbc.createWorker({ 33 | taskHandler: job => 34 | job.error('BUSINESS_ERROR', "Well, that didn't work"), 35 | taskType: 'throw-bpmn-error-task', 36 | timeout: Duration.seconds.of(30), 37 | }) 38 | zbc.createWorker({ 39 | taskType: 'sad-flow', 40 | taskHandler: job => 41 | job.complete({ 42 | bpmnErrorCaught: true, 43 | }), 44 | }) 45 | const result = await zbc.createProcessInstanceWithResult({ 46 | bpmnProcessId: processId, 47 | requestTimeout: 20000, 48 | variables: {} 49 | }) 50 | expect(result.variables.bpmnErrorCaught).toBe(true) 51 | }) 52 | 53 | test('Can set variables when throwing a BPMN Error', async () => { 54 | zbc.createWorker({ 55 | taskHandler: job => 56 | job.error({ 57 | errorCode: 'BUSINESS_ERROR', 58 | errorMessage: "Well, that didn't work", 59 | variables: {something: "someValue"} 60 | }), 61 | taskType: 'throw-bpmn-error-task', 62 | timeout: Duration.seconds.of(30), 63 | }) 64 | zbc.createWorker({ 65 | taskType: 'sad-flow', 66 | taskHandler: job => 67 | job.complete({ 68 | bpmnErrorCaught: true, 69 | }), 70 | }) 71 | const result = await zbc.createProcessInstanceWithResult({ 72 | bpmnProcessId: processId, 73 | requestTimeout: 20000, 74 | variables: {} 75 | }) 76 | expect(result.variables.bpmnErrorCaught).toBe(true) 77 | // expect(result.variables.something).toBe("someValue") 78 | }) 79 | -------------------------------------------------------------------------------- /src/__tests__/integration/Client-integration.spec.ts: -------------------------------------------------------------------------------- 1 | import { cancelProcesses } from '../../lib/cancelProcesses' 2 | import { ZBClient } from '../../index' 3 | import { CreateProcessInstanceResponse } from '../../lib/interfaces-grpc-1.0' 4 | 5 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 6 | jest.setTimeout(30000) 7 | 8 | let zbc: ZBClient 9 | let wf: CreateProcessInstanceResponse 10 | let processId: string 11 | 12 | beforeAll(async () => { 13 | const client = new ZBClient() 14 | const res = await client.deployProcess('./src/__tests__/testdata/hello-world.bpmn') 15 | processId = res.processes[0].bpmnProcessId 16 | await cancelProcesses(processId) 17 | await client.close() 18 | }) 19 | 20 | beforeEach(() => { 21 | zbc = new ZBClient() 22 | }) 23 | 24 | afterEach(async() => { 25 | await zbc.close() 26 | if (wf && wf.processInstanceKey) { 27 | await zbc.cancelProcessInstance(wf.processInstanceKey).catch(e => e) // Cleanup any active processes 28 | } 29 | await cancelProcesses(processId) 30 | }) 31 | 32 | afterAll(async () => { 33 | await zbc.close() // .then(() => console.log(`ZBClient closed`)) 34 | await cancelProcesses(processId) 35 | }) 36 | 37 | test('Can get the broker topology', async () => { 38 | const res = await zbc.topology() 39 | expect(res?.brokers).toBeTruthy() 40 | }) 41 | 42 | test('Can create a worker', async() => { 43 | const worker = zbc.createWorker({ 44 | taskType: 'TASK_TYPE', 45 | taskHandler: job => job.complete(), 46 | loglevel: 'NONE', 47 | }) 48 | expect(worker).toBeTruthy() 49 | await worker.close() 50 | }) 51 | 52 | test('Can cancel a process', async () => { 53 | const client = new ZBClient() 54 | const process = await client.createProcessInstance({ 55 | bpmnProcessId: processId, 56 | variables: {} 57 | }) 58 | const key = process.processInstanceKey 59 | expect(key).toBeTruthy() 60 | await client.cancelProcessInstance(key) 61 | try { 62 | await client.cancelProcessInstance(key) // A call to cancel a process that doesn't exist should throw 63 | } catch (e: any) { 64 | expect(1).toBe(1) 65 | } 66 | await client.close() 67 | }) 68 | 69 | test("does not retry to cancel a process instance that doesn't exist", async () => { 70 | // See: https://github.com/zeebe-io/zeebe/issues/2680 71 | // await zbc.cancelProcessInstance('123LoL') 72 | try { 73 | await zbc.cancelProcessInstance(2251799813686202) 74 | } catch (e: any) { 75 | expect(e.message.indexOf('5 NOT_FOUND:')).toBe(0) 76 | } 77 | expect.assertions(1) 78 | }) 79 | -------------------------------------------------------------------------------- /src/__tests__/integration/Client-onReady.spec.ts: -------------------------------------------------------------------------------- 1 | import { ZBClient } from '../..' 2 | 3 | jest.setTimeout(30000) 4 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 5 | 6 | test(`Doesn't call the onReady handler if there is no broker`, () => 7 | new Promise(async done => { 8 | let called = false 9 | const zbc2 = new ZBClient('localtoast: 267890', { 10 | onReady: () => { 11 | called = true 12 | }, 13 | }) // Broker doesn't exist!!! 14 | setTimeout(async () => { 15 | expect(called).toBe(false) 16 | await zbc2.close() 17 | done(null) 18 | }, 4000) 19 | })) 20 | 21 | test(`Does call the onReady handler if there is a broker and eagerConnection is true`, done => { 22 | let called = 0 23 | const zbc2 = new ZBClient({ 24 | eagerConnection: true, 25 | onReady: () => { 26 | called++ 27 | }, 28 | }) 29 | 30 | setTimeout(async () => { 31 | expect(called).toBe(1) 32 | await zbc2.close() 33 | done() 34 | }, 6000) 35 | }) 36 | 37 | test(`Does set connected to true if there is a broker`, done => { 38 | const zbc2 = new ZBClient({ 39 | eagerConnection: true, 40 | }) 41 | 42 | setTimeout(async () => { 43 | expect(zbc2.connected).toBe(true) 44 | await zbc2.close() 45 | done() 46 | }, 6000) 47 | }) 48 | 49 | test(`Does emit the ready event if there is a broker and eagerConnection: true`, done => { 50 | let called = 0 51 | const zbc2 = new ZBClient({ eagerConnection: true }) 52 | zbc2.on('ready', () => { 53 | called++ 54 | }) 55 | 56 | setTimeout(async () => { 57 | expect(called).toBe(1) 58 | expect(zbc2.connected).toBe(true) 59 | await zbc2.close() 60 | done() 61 | }, 6000) 62 | }) 63 | -------------------------------------------------------------------------------- /src/__tests__/integration/Client-setVariables.spec.ts: -------------------------------------------------------------------------------- 1 | import { cancelProcesses } from '../../lib/cancelProcesses' 2 | import { ZBClient } from '../..' 3 | import { CreateProcessInstanceResponse, DeployProcessResponse } from '../../lib/interfaces-grpc-1.0' 4 | 5 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 6 | jest.setTimeout(30000) 7 | 8 | const trace = async (result: T) => { 9 | // tslint:disable-next-line: no-console 10 | // console.log(result) 11 | return result 12 | } 13 | 14 | const zbc = new ZBClient() 15 | let wf: CreateProcessInstanceResponse 16 | let deploy: DeployProcessResponse 17 | let processId: string 18 | 19 | beforeAll(async () => { 20 | deploy = await zbc.deployProcess('./src/__tests__/testdata/conditional-pathway.bpmn') 21 | processId = deploy.processes[0].bpmnProcessId 22 | await cancelProcesses(processId) 23 | }) 24 | 25 | afterAll(() => 26 | new Promise(async done => { 27 | try { 28 | if (wf?.processInstanceKey) { 29 | zbc.cancelProcessInstance(wf.processInstanceKey) // Cleanup any active processes 30 | } 31 | } finally { 32 | await zbc.close() // Makes sure we don't forget to close connection 33 | done(null) 34 | } 35 | await cancelProcesses(processId) 36 | }) 37 | ) 38 | 39 | test('Can update process variables with setVariables', () => 40 | new Promise(async done => { 41 | jest.setTimeout(30000) 42 | 43 | wf = await zbc 44 | .createProcessInstance({ 45 | bpmnProcessId: processId, 46 | variables: { 47 | conditionVariable: true 48 | } 49 | }) 50 | .then(trace) 51 | 52 | const wfi = wf?.processInstanceKey 53 | expect(wfi).toBeTruthy() 54 | 55 | zbc.setVariables({ 56 | elementInstanceKey: wfi, 57 | local: false, 58 | variables: { 59 | conditionVariable: false, 60 | }, 61 | }).then(trace) 62 | 63 | zbc.createWorker({ 64 | taskType: 'wait', 65 | taskHandler: async job => { 66 | expect(job?.processInstanceKey).toBe(wfi) 67 | trace(`Completing wait job for ${job.processInstanceKey}`) 68 | return job.complete() 69 | }, 70 | loglevel: 'INFO', 71 | }) 72 | 73 | zbc.createWorker({ 74 | taskType: 'pathB', 75 | taskHandler: async job => { 76 | expect(job?.processInstanceKey).toBe(wfi) 77 | expect(job?.variables?.conditionVariable).toBe(false) 78 | const res1 = job.complete() 79 | done(null) 80 | return res1 81 | }, 82 | loglevel: 'INFO', 83 | }) 84 | })) 85 | -------------------------------------------------------------------------------- /src/__tests__/integration/Client-startProcess.spec.ts: -------------------------------------------------------------------------------- 1 | import { cancelProcesses } from '../../lib/cancelProcesses' 2 | import { ZBClient } from '../..' 3 | import { CreateProcessInstanceResponse } from '../../lib/interfaces-grpc-1.0' 4 | 5 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 6 | jest.setTimeout(30000) 7 | 8 | let zbc = new ZBClient() 9 | let wf: CreateProcessInstanceResponse 10 | let id: string | null 11 | let processId: string 12 | let processId2: string 13 | 14 | beforeAll(async () => { 15 | const res = await zbc.deployProcess('./src/__tests__/testdata/hello-world.bpmn') 16 | processId = res.processes[0].bpmnProcessId 17 | processId2 = (await zbc.deployProcess('./src/__tests__/testdata/Client-SkipFirstTask.bpmn')).processes[0].bpmnProcessId 18 | await cancelProcesses(processId) 19 | await cancelProcesses(processId2) 20 | await zbc.close() 21 | }) 22 | 23 | beforeEach(() => { 24 | zbc = new ZBClient() 25 | }) 26 | 27 | afterEach(async () => { 28 | if (id) { 29 | await zbc.cancelProcessInstance(id).catch(_ => _) 30 | id = null 31 | } 32 | await zbc.close() 33 | }) 34 | 35 | afterAll(async () => { 36 | if (id) { 37 | zbc.cancelProcessInstance(id).catch(_ => _) 38 | id = null 39 | } 40 | await zbc.close() // Makes sure we don't forget to close connection 41 | await cancelProcesses(processId) 42 | await cancelProcesses(processId) 43 | }) 44 | 45 | test('Can start a process', async () => { 46 | wf = await zbc.createProcessInstance({ 47 | bpmnProcessId: processId, 48 | variables: {} 49 | }) 50 | await zbc.cancelProcessInstance(wf.processInstanceKey) 51 | expect(wf.bpmnProcessId).toBe(processId) 52 | expect(wf.processInstanceKey).toBeTruthy() 53 | }) 54 | 55 | test('Can start a process at an arbitrary point', done => { 56 | const random = Math.random() 57 | const worker = zbc.createWorker({ 58 | taskType: "second_service_task", 59 | taskHandler: job => { 60 | expect(job.variables.id).toBe(random) 61 | return job.complete().then(finish) 62 | } 63 | }) 64 | const finish = () => 65 | worker.close().then(() => done()) 66 | zbc.createProcessInstance({ 67 | bpmnProcessId: 'SkipFirstTask', 68 | variables: { id: random }, 69 | startInstructions: [{elementId: 'second_service_task'}] 70 | }).then(res => (id = res.processInstanceKey)) 71 | }) 72 | -------------------------------------------------------------------------------- /src/__tests__/integration/Topology.spec.ts: -------------------------------------------------------------------------------- 1 | import { ZBClient } from '../..' 2 | 3 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 4 | 5 | const zbc = new ZBClient({ 6 | eagerConnection: false, 7 | }) 8 | 9 | test('it can get the topology', async () => { 10 | const res = await zbc.topology() 11 | expect(Array.isArray(res?.brokers)).toBe(true) 12 | await zbc.close() 13 | }) 14 | -------------------------------------------------------------------------------- /src/__tests__/integration/TypeSurfaceAreaTest.spec.ts: -------------------------------------------------------------------------------- 1 | import { ZBClient, ZBWorkerTaskHandler } from '../../index' 2 | 3 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 4 | 5 | jest.setTimeout(7000) 6 | 7 | test("Hasn't broken any public type contracts", async () => { 8 | const zbc = new ZBClient({ 9 | loglevel: 'NONE', 10 | }) 11 | const handler: ZBWorkerTaskHandler = (job, worker) => { 12 | worker.log(job.bpmnProcessId) 13 | return job.complete() 14 | } 15 | zbc.createWorker({ taskType: 'nope', taskHandler: handler }) 16 | await zbc.close() 17 | expect(true).toBeTruthy() 18 | }) 19 | -------------------------------------------------------------------------------- /src/__tests__/integration/Worker-1.0-complete.spec.ts: -------------------------------------------------------------------------------- 1 | import { cancelProcesses } from '../../lib/cancelProcesses' 2 | import { ZBClient } from '../..' 3 | import { CreateProcessInstanceResponse } from '../../lib/interfaces-grpc-1.0' 4 | 5 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 6 | jest.setTimeout(30000) 7 | const zbc = new ZBClient() 8 | let wf: CreateProcessInstanceResponse | undefined 9 | 10 | let processId1: string 11 | let processId2: string 12 | let processId3: string 13 | 14 | beforeAll(async () => { 15 | const res1 = await zbc.deployProcess('./src/__tests__/testdata/hello-world.bpmn') 16 | processId1 = res1.processes[0].bpmnProcessId 17 | await cancelProcesses(processId1) 18 | const res2 = await zbc.deployProcess('./src/__tests__/testdata/hello-world-complete.bpmn') 19 | processId2 = res2.processes[0].bpmnProcessId 20 | await cancelProcesses(processId2) 21 | const res3 = await zbc.deployProcess('./src/__tests__/testdata/conditional-pathway.bpmn') 22 | processId3 = res3.processes[0].bpmnProcessId 23 | await cancelProcesses(processId3) 24 | }) 25 | 26 | afterEach(async () => { 27 | try { 28 | if (wf?.processInstanceKey) { 29 | await zbc.cancelProcessInstance(wf.processInstanceKey).catch(e => e) 30 | } 31 | } catch { 32 | 33 | } 34 | }) 35 | 36 | afterAll(async() => { 37 | await zbc.close() 38 | await cancelProcesses(processId1) 39 | await cancelProcesses(processId2) 40 | await cancelProcesses(processId3) 41 | }) 42 | 43 | test('Can service a task', () => 44 | new Promise(async done => { 45 | wf = await zbc.createProcessInstance({ 46 | bpmnProcessId: processId1, 47 | variables: {} 48 | }) 49 | zbc.createWorker({ 50 | taskType: 'console-log', 51 | taskHandler: async job => { 52 | expect(job.processInstanceKey).toBe(wf?.processInstanceKey) 53 | const res1 = await job.complete(job.variables) 54 | done(null) 55 | return res1 56 | }, 57 | loglevel: 'NONE', 58 | }) 59 | })) 60 | 61 | test('Can service a task with complete.success', () => 62 | new Promise(async done => { 63 | wf = await zbc.createProcessInstance({ 64 | bpmnProcessId: processId2, 65 | variables: {} 66 | }) 67 | zbc.createWorker({ 68 | taskType: 'console-log-complete', 69 | taskHandler: async job => { 70 | expect(job.processInstanceKey).toBe(wf?.processInstanceKey) 71 | const res1 = await job.complete(job.variables) 72 | done(null) 73 | return res1 74 | }, 75 | loglevel: 'NONE', 76 | }) 77 | })) 78 | 79 | test('Can update process variables with complete.success()', () => 80 | new Promise(async done => { 81 | wf = await zbc.createProcessInstance({ 82 | bpmnProcessId: processId3, 83 | variables: { 84 | conditionVariable: true 85 | } 86 | }) 87 | const wfi = wf?.processInstanceKey 88 | expect(wfi).toBeTruthy() 89 | 90 | zbc.createWorker({ 91 | taskType: 'wait', 92 | taskHandler: async job => { 93 | expect(job.processInstanceKey).toBe(wfi) 94 | return job.complete({ 95 | conditionVariable: false, 96 | }) 97 | }, 98 | loglevel: 'NONE', 99 | }) 100 | 101 | zbc.createWorker({ 102 | taskType: 'pathB', 103 | taskHandler: async job => { 104 | expect(job.processInstanceKey).toBe(wfi) 105 | expect(job.variables.conditionVariable).toBe(false) 106 | const res1 = await job.complete(job.variables) 107 | wf = undefined 108 | done(null) 109 | return res1 110 | }, 111 | loglevel: 'NONE', 112 | }) 113 | })) 114 | -------------------------------------------------------------------------------- /src/__tests__/integration/Worker-BatchWorker.spec.ts: -------------------------------------------------------------------------------- 1 | import { cancelProcesses } from '../../lib/cancelProcesses' 2 | import { Duration, ZBClient } from '../..' 3 | 4 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 5 | jest.setTimeout(30000) 6 | const zbc = new ZBClient() 7 | let processId: string 8 | 9 | beforeAll(async () => { 10 | const res = await zbc.deployProcess('./src/__tests__/testdata/hello-world.bpmn') 11 | processId = res.processes[0].bpmnProcessId 12 | await cancelProcesses(processId) 13 | }) 14 | 15 | afterAll(async () => { 16 | await zbc.close() // Makes sure we don't forget to close connection 17 | await cancelProcesses(processId) 18 | }) 19 | 20 | test('BatchWorker gets ten jobs', () => 21 | new Promise(async done => { 22 | for (let i = 0; i < 10; i++) { 23 | await zbc.createProcessInstance({ 24 | bpmnProcessId: processId, 25 | variables: {} 26 | }) 27 | } 28 | 29 | const w = zbc.createBatchWorker({ 30 | jobBatchMaxTime: Duration.seconds.from(120), 31 | jobBatchMinSize: 10, 32 | loglevel: 'NONE', 33 | taskHandler: async jobs => { 34 | expect(jobs.length).toBe(10) 35 | const res1 = await Promise.all(jobs.map(job => job.complete())) 36 | await w.close() 37 | done(null) 38 | return res1 39 | }, 40 | taskType: 'console-log', 41 | }) 42 | })) 43 | -------------------------------------------------------------------------------- /src/__tests__/integration/Worker-Failure-Backoff-Retry.spec.ts: -------------------------------------------------------------------------------- 1 | import { cancelProcesses } from '../../lib/cancelProcesses' 2 | import { ZBClient } from '../..' 3 | import { CreateProcessInstanceResponse } from '../../lib/interfaces-grpc-1.0' 4 | 5 | 6 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 7 | jest.setTimeout(60000) 8 | 9 | let zbc = new ZBClient() 10 | let wf: CreateProcessInstanceResponse | undefined 11 | let processId: string 12 | 13 | beforeAll(async () => { 14 | const res = await zbc.deployProcess('./src/__tests__/testdata/Worker-Failure1.bpmn') 15 | processId = res.processes[0].bpmnProcessId 16 | await cancelProcesses(processId) 17 | }) 18 | 19 | afterEach(async () => { 20 | try { 21 | if (wf?.processInstanceKey) { 22 | await zbc.cancelProcessInstance(wf.processInstanceKey) 23 | } 24 | } catch (e: any) { 25 | // console.log('Caught NOT FOUND') // @DEBUG 26 | } finally { 27 | 28 | } 29 | }) 30 | 31 | afterAll(async () => { 32 | await zbc.close() 33 | await cancelProcesses(processId) 34 | }) 35 | 36 | test('Can specify a retryBackoff with complete.failure()', () => 37 | new Promise(async resolve => { 38 | 39 | wf = await zbc.createProcessInstance({ 40 | bpmnProcessId: processId, 41 | variables: { 42 | conditionVariable: true 43 | } 44 | }) 45 | const wfi = wf.processInstanceKey 46 | expect(wfi).toBeTruthy() 47 | 48 | await zbc.setVariables({ 49 | elementInstanceKey: wfi, 50 | local: false, 51 | variables: { 52 | conditionVariable: false, 53 | }, 54 | }) 55 | 56 | let then = new Date() 57 | const w = zbc.createWorker({ 58 | taskType: 'wait-worker-failure', 59 | taskHandler: async job => { 60 | // Succeed on the third attempt 61 | if (job.retries === 1) { 62 | const now = new Date() 63 | const res1 = await job.complete() 64 | expect(job.processInstanceKey).toBe(wfi) 65 | expect((now as any) - (then as any)).toBeGreaterThan(1800) 66 | wf = undefined 67 | 68 | zbc.cancelProcessInstance(wfi) 69 | resolve(null) 70 | await w.close() 71 | return res1 72 | } 73 | then = new Date() 74 | // Fail on the first attempt, with a 2s backoff 75 | return job.fail({ 76 | errorMessage: 77 | 'Triggering a retry with a two second back-off', 78 | retryBackOff: 2000, 79 | retries: 1, 80 | }) 81 | }, 82 | loglevel: 'NONE', 83 | }) 84 | })) 85 | -------------------------------------------------------------------------------- /src/__tests__/integration/Worker-Failure-Retries.spec.ts: -------------------------------------------------------------------------------- 1 | import { cancelProcesses } from '../../lib/cancelProcesses' 2 | import { ZBClient } from '../..' 3 | import { CreateProcessInstanceResponse } from '../../lib/interfaces-grpc-1.0' 4 | 5 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 6 | jest.setTimeout(60000) 7 | 8 | let zbc: ZBClient 9 | let wf: CreateProcessInstanceResponse | undefined 10 | 11 | beforeEach(() => { 12 | zbc = new ZBClient() 13 | }) 14 | 15 | afterEach(async () => { 16 | try { 17 | if (wf?.processInstanceKey) { 18 | await zbc.cancelProcessInstance(wf.processInstanceKey) 19 | } 20 | } catch (e: any) { 21 | // console.log('Caught NOT FOUND') // @DEBUG 22 | } finally { 23 | await zbc.close() // Makes sure we don't forget to close connection 24 | } 25 | }) 26 | 27 | test('Decrements the retries count by default', () => 28 | new Promise(async done => { 29 | const res = await zbc.deployProcess('./src/__tests__/testdata/Worker-Failure-Retries.bpmn') 30 | await cancelProcesses(res.processes[0].bpmnProcessId) 31 | wf = await zbc.createProcessInstance({ 32 | bpmnProcessId: 'worker-failure-retries', 33 | variables: { 34 | conditionVariable: true 35 | } 36 | }) 37 | let called = false 38 | 39 | const worker = zbc.createWorker({ 40 | taskType: 'service-task-worker-failure-retries', 41 | taskHandler: job => { 42 | if (!called) { 43 | expect(job.retries).toBe(100) 44 | called = true 45 | return job.fail('Some reason') 46 | } 47 | expect(job.retries).toBe(99) 48 | done(null) 49 | return job.complete().then(async res => { 50 | await worker.close() 51 | return res 52 | }) 53 | } 54 | }) 55 | }) 56 | ) 57 | 58 | test('Set the retries to a specific number when provided with one via simple signature', () => 59 | new Promise(async done => { 60 | const res = await zbc.deployProcess('./src/__tests__/testdata/Worker-Failure-Retries.bpmn') 61 | cancelProcesses(res.processes[0].bpmnProcessId) 62 | wf = await zbc.createProcessInstance({ 63 | bpmnProcessId: 'worker-failure-retries', 64 | variables: { 65 | conditionVariable: true 66 | } 67 | }) 68 | let called = false 69 | 70 | const worker = zbc.createWorker({ 71 | taskType: 'service-task-worker-failure-retries', 72 | taskHandler: job => { 73 | if (!called) { 74 | expect(job.retries).toBe(100) 75 | called = true 76 | return job.fail('Some reason', 101) 77 | } 78 | expect(job.retries).toBe(101) 79 | done(null) 80 | return job.complete().then(async res => { 81 | await worker.close() 82 | return res 83 | }) 84 | } 85 | }) 86 | }) 87 | ) 88 | 89 | test('Set the retries to a specific number when provided with one via object signature', () => 90 | new Promise(async done => { 91 | const res = await zbc.deployProcess('./src/__tests__/testdata/Worker-Failure-Retries.bpmn') 92 | await cancelProcesses(res.processes[0].bpmnProcessId) 93 | wf = await zbc.createProcessInstance({ 94 | bpmnProcessId: 'worker-failure-retries', 95 | variables: { 96 | conditionVariable: true 97 | } 98 | }) 99 | let called = false 100 | 101 | const worker = zbc.createWorker({ 102 | taskType: 'service-task-worker-failure-retries', 103 | taskHandler: job => { 104 | if (!called) { 105 | expect(job.retries).toBe(100) 106 | called = true 107 | return job.fail({ errorMessage: 'Some reason', retries: 101}) 108 | } 109 | expect(job.retries).toBe(101) 110 | done(null) 111 | return job.complete().then(async res => { 112 | await worker.close() 113 | return res 114 | }) 115 | } 116 | }) 117 | }) 118 | ) 119 | -------------------------------------------------------------------------------- /src/__tests__/integration/Worker-Failure.spec.ts: -------------------------------------------------------------------------------- 1 | import { cancelProcesses } from '../../lib/cancelProcesses' 2 | import { ZBClient } from '../..' 3 | import { CreateProcessInstanceResponse, DeployProcessResponse } from '../../lib/interfaces-grpc-1.0' 4 | 5 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 6 | jest.setTimeout(60000) 7 | 8 | const zbc = new ZBClient() 9 | let wf: CreateProcessInstanceResponse | undefined 10 | 11 | let wf1: DeployProcessResponse 12 | let wf2: DeployProcessResponse 13 | let wf3: DeployProcessResponse 14 | let bpmnProcessId1: string 15 | let bpmnProcessId2: string 16 | let bpmnProcessId3: string 17 | 18 | beforeAll(async () => { 19 | wf1 = await zbc.deployProcess('./src/__tests__/testdata/Worker-Failure1.bpmn') 20 | bpmnProcessId1 = wf1.processes[0].bpmnProcessId 21 | wf2 = await zbc.deployProcess('./src/__tests__/testdata/Worker-Failure2.bpmn') 22 | bpmnProcessId2 = wf2.processes[0].bpmnProcessId 23 | wf3 = await zbc.deployProcess('./src/__tests__/testdata/Worker-Failure3.bpmn') 24 | bpmnProcessId3 = wf3.processes[0].bpmnProcessId 25 | await cancelProcesses(bpmnProcessId1) 26 | await cancelProcesses(bpmnProcessId2) 27 | await cancelProcesses(bpmnProcessId3) 28 | }) 29 | 30 | afterEach(async () => { 31 | try { 32 | if (wf?.processInstanceKey) { 33 | await zbc.cancelProcessInstance(wf.processInstanceKey) 34 | } 35 | } catch (e: any) { 36 | // console.log('Caught NOT FOUND') // @DEBUG 37 | } 38 | }) 39 | 40 | afterAll(async() => { 41 | await zbc.close() 42 | await cancelProcesses(bpmnProcessId1) 43 | await cancelProcesses(bpmnProcessId2) 44 | await cancelProcesses(bpmnProcessId3) 45 | }) 46 | 47 | test('Causes a retry with complete.failure()', () => 48 | new Promise(async resolve => { 49 | wf = await zbc.createProcessInstance({ 50 | bpmnProcessId: bpmnProcessId1, 51 | variables: { 52 | conditionVariable: true 53 | } 54 | }) 55 | const wfi = wf.processInstanceKey 56 | expect(wfi).toBeTruthy() 57 | 58 | await zbc.setVariables({ 59 | elementInstanceKey: wfi, 60 | local: false, 61 | variables: { 62 | conditionVariable: false, 63 | }, 64 | }) 65 | 66 | zbc.createWorker({ 67 | taskType: 'wait-worker-failure', 68 | taskHandler: async job => { 69 | // Succeed on the third attempt 70 | if (job.retries === 1) { 71 | const res1 = await job.complete() 72 | expect(job.processInstanceKey).toBe(wfi) 73 | expect(job.retries).toBe(1) 74 | wf = undefined 75 | 76 | resolve(null) 77 | return res1 78 | } 79 | return job.fail('Triggering a retry') 80 | }, 81 | loglevel: 'NONE', 82 | }) 83 | })) 84 | 85 | test('Does not fail a process when the handler throws, by default', () => 86 | new Promise(async done => { 87 | wf = await zbc.createProcessInstance({ 88 | bpmnProcessId: bpmnProcessId2, 89 | variables: {} 90 | }) 91 | 92 | let alreadyFailed = false 93 | 94 | // Faulty worker - throws an unhandled exception in task handler 95 | const w = zbc.createWorker({ 96 | taskType: 'console-log-worker-failure-2', 97 | taskHandler: async job => { 98 | if (alreadyFailed) { 99 | await zbc.cancelProcessInstance(wf!.processInstanceKey) // throws if not found. Should NOT throw in this test 100 | job.complete() 101 | return w.close().then(() => done(null)) 102 | } 103 | alreadyFailed = true 104 | throw new Error( 105 | 'Unhandled exception in task handler for testing purposes' 106 | ) // Will be caught in the library 107 | }, 108 | 109 | loglevel: 'NONE', 110 | pollInterval: 10000, 111 | }) 112 | })) 113 | 114 | test('Fails a process when the handler throws and options.failProcessOnException is set', () => 115 | new Promise(async done => { 116 | 117 | wf = await zbc.createProcessInstance({ 118 | bpmnProcessId: bpmnProcessId3, 119 | variables: {} 120 | }) 121 | 122 | let alreadyFailed = false 123 | 124 | // Faulty worker 125 | const w = zbc.createWorker({ 126 | taskType: 'console-log-worker-failure-3', 127 | taskHandler: job => { 128 | if (alreadyFailed) { 129 | // It polls multiple times a second, and we need it to only throw once 130 | return job.forward() 131 | } 132 | alreadyFailed = true 133 | testProcessInstanceExists() // waits 1000ms then checks 134 | throw new Error( 135 | 'Unhandled exception in task handler for test purposes' 136 | ) // Will be caught in the library 137 | }, 138 | failProcessOnException: true, 139 | loglevel: 'NONE', 140 | }) 141 | 142 | function testProcessInstanceExists() { 143 | setTimeout(async () => { 144 | try { 145 | await zbc.cancelProcessInstance(wf!.processInstanceKey) // throws if not found. SHOULD throw in this test 146 | } catch (e: any) { 147 | w.close().then(() => done(null)) 148 | } 149 | }, 1500) 150 | } 151 | })) 152 | -------------------------------------------------------------------------------- /src/__tests__/integration/Worker-LongPoll.spec.ts: -------------------------------------------------------------------------------- 1 | import * as uuid from 'uuid' 2 | import { ZBClient } from '../..' 3 | import { cancelProcesses } from '../../lib/cancelProcesses' 4 | 5 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 6 | 7 | jest.setTimeout(40000) 8 | 9 | let processId: string 10 | 11 | const zbcLongPoll = new ZBClient({ 12 | longPoll: 60000, 13 | }) 14 | 15 | afterAll(async () => { 16 | await zbcLongPoll.close() 17 | await cancelProcesses(processId) 18 | }) 19 | 20 | beforeAll(async () => { 21 | const res = await zbcLongPoll.deployProcess('./src/__tests__/testdata/Worker-LongPoll.bpmn') 22 | processId = res.processes[0].bpmnProcessId 23 | await cancelProcesses(processId) 24 | }) 25 | 26 | test('Does long poll by default', done => { 27 | const worker = zbcLongPoll.createWorker({ 28 | taskType: uuid.v4(), 29 | taskHandler: job => job.complete(job.variables), 30 | loglevel: 'NONE', 31 | debug: true, 32 | }) 33 | // Wait to outside 10s - it should have polled once when it gets the job 34 | setTimeout(async () => { 35 | expect(worker.pollCount).toBe(1) 36 | done() 37 | }, 30000) 38 | }) 39 | -------------------------------------------------------------------------------- /src/__tests__/integration/Worker-RaiseIncident.spec.ts: -------------------------------------------------------------------------------- 1 | import { cancelProcesses } from '../../lib/cancelProcesses' 2 | import { ZBClient } from '../..' 3 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 4 | 5 | /** 6 | * Note: This test needs to be modified to leave its process instance active so the incident can be manually verified 7 | */ 8 | jest.setTimeout(30000) 9 | 10 | let processInstanceKey: string 11 | const zbc = new ZBClient() 12 | let processId: string 13 | 14 | beforeAll(async () => { 15 | const res = await zbc.deployProcess('./src/__tests__/testdata/Worker-RaiseIncident.bpmn') 16 | processId = res.processes[0].bpmnProcessId 17 | await cancelProcesses(processId) 18 | }) 19 | 20 | afterAll(async () => { 21 | zbc.cancelProcessInstance(processInstanceKey) 22 | await zbc.close() 23 | await cancelProcesses(processId) 24 | }) 25 | 26 | test('Can raise an Operate incident with complete.failure()', () => 27 | new Promise(async done => { 28 | const wf = await zbc.createProcessInstance({ 29 | bpmnProcessId: processId, 30 | variables: { 31 | conditionVariable: true 32 | } 33 | }) 34 | processInstanceKey = wf.processInstanceKey 35 | expect(processInstanceKey).toBeTruthy() 36 | 37 | await zbc.setVariables({ 38 | elementInstanceKey: processInstanceKey, 39 | local: false, 40 | variables: { 41 | conditionVariable: false, 42 | }, 43 | }) 44 | 45 | await zbc.createWorker({ 46 | taskType: 'wait-raise-incident', 47 | taskHandler: async job => { 48 | expect(job.processInstanceKey).toBe(processInstanceKey) 49 | return job.complete(job.variables) 50 | }, 51 | loglevel: 'NONE', 52 | }) 53 | 54 | await zbc.createWorker({ 55 | taskType: 'pathB-raise-incident', 56 | taskHandler: async job => { 57 | expect(job.processInstanceKey).toBe(processInstanceKey) 58 | expect(job.variables.conditionVariable).toBe(false) 59 | const res1 = await job.fail('Raise an incident in Operate', 0) 60 | // Manually verify that an incident has been raised 61 | await job.cancelWorkflow() 62 | // comment out the preceding line for the verification test 63 | done(null) 64 | return res1 65 | }, 66 | maxJobsToActivate: 1, 67 | loglevel: 'NONE', 68 | }) 69 | })) 70 | -------------------------------------------------------------------------------- /src/__tests__/integration/Worker-fetchVariable.spec.ts: -------------------------------------------------------------------------------- 1 | import { cancelProcesses } from '../../lib/cancelProcesses' 2 | import { ZBClient } from '../..' 3 | import { CreateProcessInstanceResponse } from '../../lib/interfaces-grpc-1.0' 4 | 5 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 6 | jest.setTimeout(30000) 7 | const zbc= new ZBClient() 8 | let wf: CreateProcessInstanceResponse | undefined 9 | let processId: string 10 | 11 | beforeAll(async () => { 12 | const res = await zbc.deployProcess('./src/__tests__/testdata/hello-world.bpmn') 13 | processId = res.processes[0].bpmnProcessId 14 | await cancelProcesses(processId) 15 | }) 16 | 17 | afterEach(async () => { 18 | try { 19 | if (wf?.processInstanceKey) { 20 | await zbc.cancelProcessInstance(wf.processInstanceKey).catch(e => e) 21 | } 22 | } catch { 23 | 24 | } 25 | }) 26 | 27 | afterAll(async () => { 28 | await cancelProcesses(processId) 29 | await zbc.close() 30 | }) 31 | 32 | test('Can retrieve only specified variables using fetchVariable', () => 33 | new Promise(async done => { 34 | wf = await zbc.createProcessInstance({ 35 | bpmnProcessId: processId, 36 | variables: { 37 | var1: 'foo', 38 | var2: 'bar' 39 | } 40 | }) 41 | 42 | zbc.createWorker({ 43 | fetchVariable: ['var2'], 44 | taskType: 'console-log', 45 | taskHandler: async job => { 46 | expect(job.processInstanceKey).toBe(wf?.processInstanceKey) 47 | expect(job.variables.var2).toEqual('bar') 48 | expect((job.variables as any).var1).not.toBeDefined() 49 | const res1 = await job.complete(job.variables) 50 | done(null) 51 | return res1 52 | }, 53 | loglevel: 'NONE', 54 | }) 55 | })) 56 | -------------------------------------------------------------------------------- /src/__tests__/integration/Worker-integration.spec.ts: -------------------------------------------------------------------------------- 1 | import { cancelProcesses } from '../../lib/cancelProcesses' 2 | import { ZBClient } from '../..' 3 | import { CreateProcessInstanceResponse } from '../../lib/interfaces-grpc-1.0' 4 | 5 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 6 | jest.setTimeout(30000) 7 | const zbc = new ZBClient() 8 | let wf: CreateProcessInstanceResponse | undefined 9 | let processId1: string 10 | let processId2: string 11 | let processId3: string 12 | 13 | beforeAll(async () => { 14 | const res1 = await zbc.deployProcess('./src/__tests__/testdata/hello-world.bpmn') 15 | processId1 = res1.processes[0].bpmnProcessId 16 | await cancelProcesses(processId1) 17 | const res2 = await zbc.deployProcess('./src/__tests__/testdata/hello-world-complete.bpmn') 18 | processId2 = res2.processes[0].bpmnProcessId 19 | await cancelProcesses(processId2) 20 | const res3 = await zbc.deployProcess('./src/__tests__/testdata/conditional-pathway.bpmn') 21 | processId3 = res3.processes[0].bpmnProcessId 22 | await cancelProcesses(processId3) 23 | }) 24 | 25 | afterEach(async () => { 26 | try { 27 | if (wf?.processInstanceKey) { 28 | await zbc.cancelProcessInstance(wf.processInstanceKey).catch(e => e) 29 | } 30 | } catch { 31 | } 32 | }) 33 | 34 | afterAll(async () => { 35 | await cancelProcesses(processId1) 36 | await cancelProcesses(processId2) 37 | await cancelProcesses(processId3) 38 | await zbc.close() 39 | }) 40 | 41 | test('Can service a task', () => 42 | new Promise(async done => { 43 | wf = await zbc.createProcessInstance({ 44 | bpmnProcessId: processId1, 45 | variables: {} 46 | }) 47 | zbc.createWorker({ 48 | taskType: 'console-log', 49 | taskHandler: async job => { 50 | expect(job.processInstanceKey).toBe(wf?.processInstanceKey) 51 | const res1 = await job.complete(job.variables) 52 | done(null) 53 | return res1 54 | }, 55 | loglevel: 'NONE', 56 | }) 57 | })) 58 | 59 | test('Can service a task with complete.success', () => 60 | new Promise(async done => { 61 | wf = await zbc.createProcessInstance({ 62 | bpmnProcessId: processId2, 63 | variables: {} 64 | }) 65 | zbc.createWorker({ 66 | taskType: 'console-log-complete', 67 | taskHandler: async job => { 68 | expect(job.processInstanceKey).toBe(wf?.processInstanceKey) 69 | const res1 = await job.complete(job.variables) 70 | done(null) 71 | return res1 72 | }, 73 | loglevel: 'NONE', 74 | }) 75 | })) 76 | 77 | test('Can update process variables with complete.success()', () => 78 | new Promise(async done => { 79 | wf = await zbc.createProcessInstance({ 80 | bpmnProcessId: processId3, 81 | variables: { 82 | conditionVariable: true 83 | } 84 | }) 85 | const wfi = wf?.processInstanceKey 86 | expect(wfi).toBeTruthy() 87 | zbc.createWorker({ 88 | taskType: 'wait', 89 | taskHandler: async job => { 90 | expect(job.processInstanceKey).toBe(wfi) 91 | return job.complete({ 92 | conditionVariable: false, 93 | }) 94 | }, 95 | loglevel: 'NONE', 96 | }) 97 | 98 | zbc.createWorker({ 99 | taskType: 'pathB', 100 | taskHandler: async job => { 101 | expect(job.processInstanceKey).toBe(wfi) 102 | expect(job.variables.conditionVariable).toBe(false) 103 | const res1 = await job.complete(job.variables) 104 | wf = undefined 105 | done(null) 106 | return res1 107 | }, 108 | loglevel: 'NONE', 109 | }) 110 | })) 111 | -------------------------------------------------------------------------------- /src/__tests__/integration/Worker-onReady.spec.ts: -------------------------------------------------------------------------------- 1 | import { ZBClient } from '../..' 2 | 3 | jest.setTimeout(40000) 4 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 5 | 6 | test(`Worker emits the ready event once if there is a broker`, done => { 7 | let called = 0 8 | const zbc2 = new ZBClient() 9 | zbc2.createWorker({ 10 | taskHandler: job => job.complete(), 11 | taskType: 'nonsense-task', 12 | }).on('ready', () => { 13 | called++ 14 | }) 15 | setTimeout(async () => { 16 | expect(called).toBe(1) 17 | await zbc2.close() 18 | done() 19 | }, 12000) 20 | }) 21 | 22 | test(`Does set connected: true if there is a broker and eagerConnection: true`, done => { 23 | const zbc2 = new ZBClient({ 24 | eagerConnection: true, 25 | }) 26 | setTimeout(async () => { 27 | expect(zbc2.connected).toBe(true) 28 | await zbc2.close() 29 | done() 30 | }, 7000) 31 | }) 32 | 33 | test(`Does not set connected: true if there is a broker and eagerConnection: false`, done => { 34 | const zbc2 = new ZBClient() 35 | setTimeout(async () => { 36 | expect(zbc2.connected).toBe(false) 37 | await zbc2.close() 38 | done() 39 | }, 7000) 40 | }) 41 | 42 | test(`Does not call the onReady handler if there is no broker`, done => { 43 | let called = 0 44 | const zbc2 = new ZBClient('nobroker') 45 | zbc2.createWorker({ 46 | onReady: () => { 47 | called++ 48 | }, 49 | taskHandler: job => job.complete(), 50 | taskType: 'nonsense-task', 51 | }) 52 | setTimeout(async () => { 53 | expect(called).toBe(0) 54 | await zbc2.close() 55 | done() 56 | }, 5000) 57 | }) 58 | 59 | test(`Does not emit the ready event if there is no broker`, done => { 60 | let called = 0 61 | const zbc2 = new ZBClient('nobroker') 62 | zbc2.createWorker({ 63 | loglevel: 'NONE', 64 | taskHandler: job => job.complete(), 65 | taskType: 'nonsense-task', 66 | }).on('ready', () => { 67 | called++ 68 | }) 69 | setTimeout(async () => { 70 | expect(called).toBe(0) 71 | await zbc2.close() 72 | done() 73 | }, 5000) 74 | }) 75 | 76 | test(`Worker calls the onReady handler once if there is a broker`, done => { 77 | let called = 0 78 | const zbc2 = new ZBClient() 79 | zbc2.createWorker({ 80 | onReady: () => { 81 | called++ 82 | }, 83 | taskHandler: job => job.complete(), 84 | taskType: 'nonsense-task', 85 | }) 86 | setTimeout(async () => { 87 | expect(called).toBe(1) 88 | await zbc2.close() 89 | done() 90 | }, 12000) 91 | }) 92 | -------------------------------------------------------------------------------- /src/__tests__/local-integration/OnConnectionError.spec.ts: -------------------------------------------------------------------------------- 1 | import { ZBClient } from '../..' 2 | 3 | jest.setTimeout(16000) 4 | process.env.ZEEBE_NODE_LOG_LEVEL = process.env.ZEEBE_NODE_LOG_LEVEL || 'NONE' 5 | 6 | xtest(`Calls the onConnectionError handler if there is no broker and eagerConnection:true`, () => 7 | new Promise(async done => { 8 | let calledA = 0 9 | const zbc2 = new ZBClient('localtoast: 267890', { 10 | eagerConnection: true, 11 | onConnectionError: () => { 12 | calledA++ 13 | }, 14 | }) 15 | setTimeout(async () => { 16 | expect(calledA).toBe(1) 17 | await zbc2.close() 18 | done(null) 19 | }, 5000) 20 | })) 21 | 22 | xtest(`Does not call the onConnectionError handler if there is a broker`, () => 23 | new Promise(done => { 24 | let calledB = 0 25 | const zbc2 = new ZBClient({ 26 | onConnectionError: () => { 27 | // tslint:disable-next-line: no-debugger 28 | debugger 29 | calledB++ 30 | // tslint:disable-next-line: no-console 31 | console.log( 32 | 'onConnection Error was called when there *is* a broker' 33 | ) 34 | // throw new Error( 35 | // 'onConnection Error was called when there *is* a broker' 36 | // ) 37 | }, 38 | }) 39 | setTimeout(async () => { 40 | expect(calledB).toBe(0) 41 | await zbc2.close() 42 | done(null) 43 | }, 5000) 44 | })) 45 | 46 | xtest(`Calls ZBClient onConnectionError once when there is no broker, eagerConnection:true, and workers with no handler`, () => 47 | new Promise(done => { 48 | let calledC = 0 49 | const zbc2 = new ZBClient('localtoast:234532534', { 50 | eagerConnection: true, 51 | onConnectionError: () => { 52 | calledC++ 53 | }, 54 | }) 55 | zbc2.createWorker({ 56 | taskType: 'whatever', 57 | taskHandler: job => job.complete(), 58 | }) 59 | zbc2.createWorker({ 60 | taskType: 'whatever', 61 | taskHandler: job => job.complete(), 62 | }) 63 | setTimeout(() => { 64 | zbc2.close() 65 | expect(calledC).toBe(1) 66 | done(null) 67 | }, 10000) 68 | })) 69 | 70 | xtest(`Calls ZBClient onConnectionError when there no broker, for the client and each worker with a handler`, () => 71 | new Promise(async done => { 72 | let calledD = 0 73 | const zbc2 = new ZBClient('localtoast:234532534', { 74 | onConnectionError: () => { 75 | calledD++ 76 | }, 77 | }) 78 | zbc2.createWorker({ 79 | taskType: 'whatever', 80 | taskHandler: job => job.complete(), 81 | onConnectionError: () => calledD++, 82 | }) 83 | setTimeout(() => { 84 | zbc2.close() 85 | expect(calledD).toBe(2) 86 | done(null) 87 | }, 10000) 88 | })) 89 | 90 | xtest(`Debounces onConnectionError`, () => 91 | new Promise(async done => { 92 | let called = 0 93 | const zbc2 = new ZBClient('localtoast:234532534', { 94 | onConnectionError: () => { 95 | called++ 96 | }, 97 | }) 98 | zbc2.createWorker({ 99 | taskType: 'whatever', 100 | taskHandler: job => job.complete(), 101 | onConnectionError: () => called++, 102 | }) 103 | setTimeout(() => { 104 | zbc2.close() 105 | expect(called).toBe(2) // toBeLessThanOrEqual(1) 106 | done(null) 107 | }, 15000) 108 | })) 109 | 110 | xtest(`Trailing parameter worker onConnectionError handler API works`, () => 111 | new Promise(done => { 112 | let calledE = 0 113 | const zbc2 = new ZBClient('localtoast:234532534', {}) 114 | zbc2.createWorker({ 115 | taskType: 'whatever', 116 | taskHandler: job => job.complete(), 117 | onConnectionError: () => calledE++, 118 | }) 119 | setTimeout(async () => { 120 | await zbc2.close() 121 | expect(calledE).toBe(1) 122 | done(null) 123 | }, 10000) 124 | })) 125 | 126 | xtest(`Does not call the onConnectionError handler if there is a business error`, () => 127 | new Promise(async done => { 128 | let calledF = 0 129 | let wf = 'arstsrasrateiuhrastulyharsntharsie' 130 | const zbc2 = new ZBClient({ 131 | onConnectionError: () => { 132 | // tslint:disable-next-line: no-console 133 | console.log('OnConnectionError!!!! Incrementing calledF') // @DEBUG 134 | const e = new Error() 135 | // tslint:disable-next-line: no-console 136 | console.log(e.stack) // @DEBUG 137 | calledF++ 138 | }, 139 | }) 140 | 141 | zbc2.createProcessInstance({ 142 | bpmnProcessId: wf, 143 | variables: {} 144 | }).catch(() => { 145 | wf = 'throw error away' 146 | }) 147 | setTimeout(async () => { 148 | expect(zbc2.connected).toBe(true) 149 | expect(calledF).toBe(0) 150 | await zbc2.close() 151 | done(null) 152 | }, 10000) 153 | })) 154 | -------------------------------------------------------------------------------- /src/__tests__/stringifyVariables.spec.ts: -------------------------------------------------------------------------------- 1 | import { parseVariables, stringifyVariables } from '../lib/stringifyVariables' 2 | 3 | // tslint:disable:object-literal-sort-keys 4 | const jobObject = { 5 | key: '37274', 6 | jobHeaders: { 7 | bpmnProcessId: 'parallel-subtasks', 8 | elementId: 'ServiceTask_0i7cog1', 9 | elementInstanceKey: '37270', 10 | workflowDefinitionVersion: 1, 11 | workflowInstanceKey: '34027', 12 | workflowKey: '1', 13 | }, 14 | customHeaders: {}, 15 | worker: '26fe8907-f518-4f8d-bd75-06acaec3c154', 16 | retries: 3, 17 | deadline: '1547595187455', 18 | variables: { 19 | jobId: '7ead71d8-30c9-4eda-81e7-f2ada6d7d0da', 20 | subtaskCount: 200, 21 | tasksCompleted: null, 22 | }, 23 | type: 'sub-task', 24 | } 25 | 26 | const expectedStringifiedVariables = 27 | '{"jobId":"7ead71d8-30c9-4eda-81e7-f2ada6d7d0da","subtaskCount":200,"tasksCompleted":null}' 28 | 29 | const jobDictionary = { 30 | key: '37274', 31 | customHeaders: {}, 32 | worker: '26fe8907-f518-4f8d-bd75-06acaec3c154', 33 | retries: 3, 34 | deadline: '1547595187455', 35 | variables: 36 | '{"jobId":"7ead71d8-30c9-4eda-81e7-f2ada6d7d0da","subtaskCount":200,"tasksCompleted":null}', 37 | type: 'sub-task', 38 | } 39 | 40 | test('stringifyVariables returns a new object', () => { 41 | expect(stringifyVariables(jobObject)).not.toEqual(jobObject) 42 | }) 43 | 44 | test('stringifyVariables stringifies the variables key of a job object', () => { 45 | const stringified = stringifyVariables(jobObject) 46 | expect(typeof stringified.variables).toBe('string') 47 | expect(stringified.variables).toBe(expectedStringifiedVariables) 48 | }) 49 | 50 | test('parseVariables returns a new object', () => { 51 | expect(parseVariables(jobDictionary)).not.toEqual(jobDictionary) 52 | }) 53 | 54 | test('parseVariables parses the payload key of a job object to JSON', () => { 55 | expect(typeof parseVariables(jobDictionary).variables).toBe('object') 56 | }) 57 | 58 | test('parseVariables correctly parses the payload string', () => { 59 | const parsed = parseVariables(jobDictionary) 60 | expect(parsed.variables.jobId).toEqual( 61 | '7ead71d8-30c9-4eda-81e7-f2ada6d7d0da' 62 | ) 63 | expect(parsed.variables.subtaskCount).toEqual(200) 64 | expect(parsed.variables.tasksCompleted).toBeNull() 65 | expect(Object.keys(parsed.variables).length).toBe(3) 66 | }) 67 | 68 | test('parseVariables returns an object with all the keys of the original', () => { 69 | const parsed = parseVariables(jobDictionary) 70 | expect(Object.keys(parsed).length).toBe(7) 71 | expect(parsed.key).toBe('37274') 72 | expect(parsed.worker).toBe('26fe8907-f518-4f8d-bd75-06acaec3c154') 73 | }) 74 | -------------------------------------------------------------------------------- /src/__tests__/testdata/Client-BrokenBpmn.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SequenceFlow_0fp53hs 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | SequenceFlow_0fp53hs 15 | SequenceFlow_112zghv 16 | 17 | 18 | 19 | SequenceFlow_17rbbvu 20 | 21 | 22 | SequenceFlow_112zghv 23 | SequenceFlow_17rbbvu 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/__tests__/testdata/Client-MessageStart.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Flow_06412wm 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Flow_12d1ngd 17 | Flow_06412wm 18 | 19 | 20 | Flow_12d1ngd 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/__tests__/testdata/Client-SkipFirstTask.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flow_13h0ndl 6 | 7 | 8 | 9 | Flow_15zn6mn 10 | 11 | 12 | 13 | 14 | 15 | 16 | Flow_027zokm 17 | Flow_15zn6mn 18 | 19 | 20 | 21 | 22 | 23 | Flow_13h0ndl 24 | Flow_027zokm 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/__tests__/testdata/Signal.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flow_0dwell7 6 | 7 | 8 | 9 | 10 | Flow_16lx2ly 11 | 12 | 13 | 14 | 15 | 16 | 17 | Flow_0dwell7 18 | Flow_16lx2ly 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/__tests__/testdata/Worker-Failure-Retries.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SequenceFlow_0fp53hs 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | SequenceFlow_0fp53hs 15 | SequenceFlow_112zghv 16 | 17 | 18 | 19 | SequenceFlow_112zghv 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/__tests__/testdata/Worker-Failure2.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SequenceFlow_0fp53hs 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | SequenceFlow_0fp53hs 15 | SequenceFlow_112zghv 16 | 17 | 18 | 19 | SequenceFlow_17rbbvu 20 | 21 | 22 | SequenceFlow_112zghv 23 | SequenceFlow_17rbbvu 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/__tests__/testdata/Worker-Failure3.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SequenceFlow_0fp53hs 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | SequenceFlow_0fp53hs 15 | SequenceFlow_112zghv 16 | 17 | 18 | 19 | SequenceFlow_17rbbvu 20 | 21 | 22 | SequenceFlow_112zghv 23 | SequenceFlow_17rbbvu 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/__tests__/testdata/Worker-LongPoll.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SequenceFlow_0fp53hs 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | SequenceFlow_0fp53hs 15 | SequenceFlow_112zghv 16 | 17 | 18 | 19 | SequenceFlow_17rbbvu 20 | 21 | 22 | SequenceFlow_112zghv 23 | SequenceFlow_17rbbvu 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/__tests__/testdata/await-outcome-long.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SequenceFlow_0xqfx4k 6 | 7 | 8 | SequenceFlow_0xqfx4k 9 | SequenceFlow_0cji6zj 10 | 11 | PT20S 12 | 13 | 14 | 15 | SequenceFlow_0cji6zj 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/__tests__/testdata/await-outcome.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SequenceFlow_0xqfx4k 6 | 7 | 8 | SequenceFlow_0xqfx4k 9 | SequenceFlow_0cji6zj 10 | 11 | PT1S 12 | 13 | 14 | 15 | SequenceFlow_0cji6zj 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/__tests__/testdata/await-outcome.bpmn.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camunda-community-hub/zeebe-client-node-js/7969ce1808c96a87519cb1a3f279287f30637c4b/src/__tests__/testdata/await-outcome.bpmn.zip -------------------------------------------------------------------------------- /src/__tests__/testdata/decision.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | season 8 | 9 | 10 | 11 | 12 | 13 | "Fall" 14 | 15 | 16 | "Potato fries" 17 | 18 | 19 | 20 | 21 | "Winter" 22 | 23 | 24 | "Roast Apple" 25 | 26 | 27 | 28 | 29 | "Spring" 30 | 31 | 32 | "Steak" 33 | 34 | 35 | 36 | 37 | "Summer" 38 | 39 | 40 | "Light Salad and Kombucha" 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/__tests__/testdata/disconnection.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SequenceFlow_1xgy4qo 6 | 7 | 8 | 9 | 10 | 11 | 12 | SequenceFlow_1xgy4qo 13 | SequenceFlow_0wp39y1 14 | 15 | 16 | SequenceFlow_0wp39y1 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/__tests__/testdata/form_1.form: -------------------------------------------------------------------------------- 1 | { 2 | "components": [ 3 | { 4 | "label": "Number", 5 | "type": "number", 6 | "layout": { 7 | "row": "Row_1h8ufhy", 8 | "columns": null 9 | }, 10 | "id": "Field_1qlk4je", 11 | "key": "field_19ab8r7" 12 | }, 13 | { 14 | "label": "Text area", 15 | "type": "textarea", 16 | "layout": { 17 | "row": "Row_08n9bxz", 18 | "columns": null 19 | }, 20 | "id": "Field_1xphwg1", 21 | "key": "field_0t0ghcb" 22 | }, 23 | { 24 | "label": "Checkbox", 25 | "type": "checkbox", 26 | "layout": { 27 | "row": "Row_0shlcir", 28 | "columns": null 29 | }, 30 | "id": "Field_07ux5ad", 31 | "key": "field_0oqjpgs" 32 | } 33 | ], 34 | "type": "default", 35 | "id": "Form_07zfo8o", 36 | "executionPlatform": "Camunda Cloud", 37 | "executionPlatformVersion": "8.2.0", 38 | "exporter": { 39 | "name": "Camunda Modeler", 40 | "version": "5.14.0" 41 | }, 42 | "schemaVersion": 10 43 | } -------------------------------------------------------------------------------- /src/__tests__/testdata/generic-test.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SequenceFlow_008b7bk 6 | 7 | 8 | 9 | SequenceFlow_06y8siu 10 | 11 | 12 | 13 | 14 | 15 | 16 | SequenceFlow_008b7bk 17 | SequenceFlow_06y8siu 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/__tests__/testdata/hello-world-complete.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SequenceFlow_0fp53hs 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | SequenceFlow_0fp53hs 15 | SequenceFlow_112zghv 16 | 17 | 18 | 19 | SequenceFlow_112zghv 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/__tests__/testdata/hello-world.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SequenceFlow_0fp53hs 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | SequenceFlow_0fp53hs 15 | SequenceFlow_112zghv 16 | 17 | 18 | 19 | SequenceFlow_112zghv 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/__tests__/testdata/quarantine-duration.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | employment_category 13 | 14 | 15 | "critical infrastructure","non-critical infrastructure" 16 | 17 | 18 | 19 | 20 | age 21 | 22 | 23 | 24 | 25 | People who are 30 and under recover faster, and we need to keep critical infrastructure running 26 | 27 | "critical infrastructure" 28 | 29 | 30 | <=30 31 | 32 | 33 | "P5D" 34 | 35 | 36 | 37 | People who are 30 and under recover faster 38 | 39 | "non-critical infrastructure" 40 | 41 | 42 | <=30 43 | 44 | 45 | "P7D" 46 | 47 | 48 | 49 | People who are over 30 take longer to recover 50 | 51 | 52 | 53 | 54 | >30 55 | 56 | 57 | "P10D" 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { ZBJsonLogger } from './lib/ZBJsonLogger' 2 | export { BpmnParser } from './lib/BpmnParser' 3 | export * from './lib/interfaces-1.0' 4 | export * from './lib/interfaces-published-contract' 5 | export * from './lib/interfaces-grpc-1.0' 6 | export * from './zb/ZBClient' 7 | export * from './zb/ZBWorker' 8 | export { ZBLogger } from './lib/ZBLogger' 9 | export { ZBSimpleLogger } from './lib/SimpleLogger' 10 | export { Duration } from 'typed-duration' 11 | -------------------------------------------------------------------------------- /src/lib/ConnectionFactory.ts: -------------------------------------------------------------------------------- 1 | import { GrpcClientCtor } from './GrpcClient' 2 | import { GrpcMiddleware } from './GrpcMiddleware' 3 | import { ZBLoggerConfig } from './interfaces-1.0' 4 | import { StatefulLogInterceptor } from './StatefulLogInterceptor' 5 | 6 | export type GrpcConnectionProfile = 'CAMUNDA_CLOUD' | 'VANILLA' 7 | export interface Characteristics { 8 | startupTime: number 9 | _tag: GrpcConnectionProfile 10 | } 11 | 12 | export const ConnectionCharacteristics: { 13 | [key in GrpcConnectionProfile]: Characteristics 14 | } = { 15 | CAMUNDA_CLOUD: { 16 | _tag: 'CAMUNDA_CLOUD', 17 | startupTime: parseInt( 18 | process.env.ZEEBE_INITIAL_CONNECTION_TOLERANCE || '6000', 19 | 10 20 | ), 21 | }, 22 | VANILLA: { 23 | _tag: 'VANILLA', 24 | startupTime: parseInt( 25 | process.env.ZEEBE_INITIAL_CONNECTION_TOLERANCE || '0', 26 | 10 27 | ), 28 | }, 29 | } 30 | 31 | export type State = 'ERROR' | 'CONNECTED' | 'UNKNOWN' 32 | 33 | export class ConnectionFactory { 34 | public static getGrpcClient({ 35 | grpcConfig, 36 | logConfig, 37 | }: { 38 | grpcConfig: GrpcClientCtor 39 | logConfig: ZBLoggerConfig 40 | }) { 41 | const characteristics = ConnectionFactory.getCharacteristics( 42 | grpcConfig.host 43 | ) 44 | const log = new StatefulLogInterceptor({ characteristics, logConfig }) 45 | const grpcClient = new GrpcMiddleware({ 46 | characteristics, 47 | config: grpcConfig, 48 | log, 49 | }).getGrpcClient() 50 | const _close = grpcClient.close.bind(grpcClient) 51 | grpcClient.close = async () => { 52 | log.close() 53 | _close() 54 | return null 55 | } 56 | 57 | return { grpcClient, log } 58 | } 59 | 60 | private static getCharacteristics(host: string): Characteristics { 61 | const isCamundaCloud = host.includes('zeebe.camunda.io') 62 | const profile = isCamundaCloud ? 'CAMUNDA_CLOUD' : 'VANILLA' 63 | return ConnectionCharacteristics[profile] 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/lib/EnvFunction.ts: -------------------------------------------------------------------------------- 1 | type EnvFunction = , K extends string>( 2 | keys: T 3 | ) => { 4 | [key1 in T[number]]: string 5 | } 6 | 7 | export const getEnv: EnvFunction = keys => { 8 | return keys.reduce( 9 | (prev, current) => ({ 10 | ...prev, 11 | [current]: process.env[current], 12 | }), 13 | {} as any 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/GrpcError.ts: -------------------------------------------------------------------------------- 1 | export const GrpcError = { 2 | OK: 0 as 0, 3 | CANCELLED: 1 as 1, 4 | UNKNOWN: 2 as 2, 5 | INVALID_ARGUMENT: 3 as 3, 6 | DEADLINE_EXCEEDED: 4 as 4, 7 | NOT_FOUND: 5 as 5, 8 | ALREADY_EXISTS: 6 as 6, 9 | PERMISSION_DENIED: 7 as 7, 10 | UNAUTHENTICATED: 16 as 16, 11 | RESOURCE_EXHAUSTED: 8 as 8, 12 | FAILED_PRECONDITION: 9 as 9, 13 | ABORTED: 10 as 10, 14 | OUT_OF_RANGE: 11 as 11, 15 | UNIMPLEMENTED: 12 as 12, 16 | INTERNAL: 13 as 13, 17 | UNAVAILABLE: 14 as 14, 18 | DATA_LOSS: 15 as 15, 19 | }; 20 | -------------------------------------------------------------------------------- /src/lib/GrpcMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionStatusEvent } from '../zb/ZBClient' 2 | import { Characteristics, State } from './ConnectionFactory' 3 | import { GrpcClient, GrpcClientCtor, MiddlewareSignals } from './GrpcClient' 4 | import { StatefulLogInterceptor } from './StatefulLogInterceptor' 5 | 6 | export class GrpcMiddleware { 7 | public blocking: boolean 8 | public state: State 9 | public log: StatefulLogInterceptor 10 | private grpcClient: GrpcClient 11 | private characteristics: Characteristics 12 | private blockingTimer?: NodeJS.Timeout 13 | 14 | constructor({ 15 | characteristics, 16 | config, 17 | log, 18 | }: { 19 | characteristics: Characteristics 20 | config: GrpcClientCtor 21 | log: StatefulLogInterceptor 22 | }) { 23 | this.characteristics = characteristics 24 | this.blocking = this.characteristics.startupTime > 0 25 | this.state = 'UNKNOWN' 26 | log.logDebug(`Grpc Middleware blocking: ${this.blocking}`) 27 | if (this.blocking) { 28 | this.blockingTimer = setTimeout(() => { 29 | this.blocking = false 30 | log.logDebug(`Grpc Middleware state: ${this.state}`) 31 | if (this.state === 'ERROR') { 32 | this.emitError(new Error(`Did not establish connection before deadline ${this.characteristics.startupTime}ms`)) 33 | } else if (this.state === 'CONNECTED') { 34 | this.emitReady() 35 | } else if (this.state === 'UNKNOWN') { 36 | this.grpcClient.emit(ConnectionStatusEvent.unknown) 37 | } 38 | }, this.characteristics.startupTime) 39 | } 40 | this.log = log 41 | this.grpcClient = this.createInterceptedGrpcClient(config) 42 | } 43 | public getGrpcClient = () => this.grpcClient 44 | 45 | private createInterceptedGrpcClient(config: GrpcClientCtor) { 46 | const grpcClient = new GrpcClient(config) 47 | const logInterceptor = this.log 48 | const _close = grpcClient.close.bind(grpcClient) 49 | grpcClient.close = async () => { 50 | if (this.blockingTimer) { 51 | clearTimeout(this.blockingTimer) 52 | } 53 | _close() 54 | return null 55 | } 56 | grpcClient.on(MiddlewareSignals.Log.Debug, logInterceptor.logDebug) 57 | grpcClient.on(MiddlewareSignals.Log.Info, logInterceptor.logInfo) 58 | grpcClient.on(MiddlewareSignals.Log.Error, logInterceptor.logError) 59 | grpcClient.on(MiddlewareSignals.Event.Error, (err) => { 60 | this.state = 'ERROR' 61 | logInterceptor.connectionError() 62 | if (!this.blocking) { 63 | this.emitError(err) 64 | } 65 | }) 66 | grpcClient.on(MiddlewareSignals.Event.Ready, () => { 67 | this.state = 'CONNECTED' 68 | logInterceptor.ready() 69 | if (!this.blocking) { 70 | this.emitReady() 71 | logInterceptor.logDebug(`Middleware emits ready`) 72 | } else { 73 | logInterceptor.logDebug(`Blocked ready emit`) 74 | } 75 | }) 76 | grpcClient.on( 77 | MiddlewareSignals.Event.GrpcInterceptError, 78 | this.handleExceptionalGrpc 79 | ) 80 | return grpcClient 81 | } 82 | 83 | private emitError = (err: Error) => 84 | this.grpcClient.emit(ConnectionStatusEvent.connectionError, err) 85 | private emitReady = () => this.grpcClient.emit(ConnectionStatusEvent.ready) 86 | private handleExceptionalGrpc = ({ 87 | callStatus, 88 | options, 89 | }: { 90 | callStatus: GrpcCallStatus 91 | options: GrpcOptions 92 | }) => { 93 | if (options.method_definition.path === 'not-happening') { 94 | this.log.logDebug( 95 | 'This is to stop the compiler choking on an unused parameter while I figure out which cases to handle.' 96 | ) 97 | } 98 | if (callStatus.code === 1 && callStatus.details.includes('503')) { 99 | this.log.logError( 100 | 'The gateway returned HTTP Error 503 (Bad Gateway). This can be a transient failure while a Kubernetes node in Camunda Cloud is being pre-empted.' 101 | ) 102 | } 103 | } 104 | } 105 | 106 | interface GrpcCallStatus { 107 | code: number 108 | details: string 109 | } 110 | interface GrpcOptions { 111 | method_definition: { 112 | /** The full path of the call, i.e. '/gateway_protocol.Gateway/SetVariables' */ 113 | path: string 114 | requestStream: boolean 115 | responseStream: boolean 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/lib/JobBatcher.ts: -------------------------------------------------------------------------------- 1 | import { ZBBatchWorker } from '../zb/ZBBatchWorker' 2 | import { 3 | BatchedJob, 4 | ICustomHeaders, 5 | IInputVariables, 6 | IOutputVariables, 7 | ZBBatchWorkerTaskHandler, 8 | } from './interfaces-1.0' 9 | import { Queue } from './Queue' 10 | 11 | export class JobBatcher { 12 | private batchedJobs: Queue = new Queue() 13 | private handler: ZBBatchWorkerTaskHandler 14 | private timeout: number 15 | private batchSize: number 16 | private worker: ZBBatchWorker 17 | private batchExecutionTimerHandle: any 18 | constructor({ 19 | handler, 20 | timeout, 21 | batchSize, 22 | worker, 23 | }: { 24 | handler: ZBBatchWorkerTaskHandler 25 | timeout: number 26 | batchSize: number 27 | worker: ZBBatchWorker 28 | }) { 29 | this.handler = handler 30 | this.timeout = timeout 31 | this.batchSize = batchSize 32 | this.worker = worker 33 | } 34 | 35 | public batch( 36 | batch: Array< 37 | BatchedJob 38 | > 39 | ) { 40 | if (!this.batchExecutionTimerHandle) { 41 | this.batchExecutionTimerHandle = setTimeout( 42 | () => this.execute(), 43 | this.timeout * 1000 44 | ) 45 | } 46 | batch.forEach(this.batchedJobs.push) 47 | if (this.batchedJobs.length() >= this.batchSize) { 48 | clearTimeout(this.batchExecutionTimerHandle) 49 | this.execute() 50 | } 51 | } 52 | 53 | private execute() { 54 | this.batchExecutionTimerHandle = undefined 55 | this.worker.debug( 56 | `Executing batched handler with ${this.batchedJobs.length()} jobs` 57 | ) 58 | try { 59 | this.handler(this.batchedJobs.drain(), this.worker) 60 | } catch (e: any) { 61 | this.worker.error( 62 | `An unhandled exception occurred in the worker task handler!` 63 | ) 64 | this.worker.error(e.message) 65 | this.worker.error(e) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/lib/MockStdOut.ts: -------------------------------------------------------------------------------- 1 | import { ZBCustomLogger } from './interfaces-published-contract' 2 | export class MockStdOut implements ZBCustomLogger { 3 | public messages: string[] = [] 4 | 5 | public info(message: string) { 6 | this.messages.push(message) 7 | } 8 | 9 | public error(message: string) { 10 | this.messages.push(message) 11 | } 12 | 13 | public debug(message: string) { 14 | this.messages.push(message) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/Queue.ts: -------------------------------------------------------------------------------- 1 | export class Queue { 2 | private q: T[] = [] 3 | public push = (element: T) => this.q.push(element) 4 | 5 | public pop = (): T | undefined => this.q.shift() 6 | 7 | public isEmpty = (): boolean => this.q.length > 0 8 | 9 | public drain = () => this.q.splice(0, this.q.length) 10 | 11 | public length = (): number => this.q.length 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/SimpleLogger.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import { Loglevel } from './interfaces-published-contract' 3 | 4 | export interface Logger { 5 | error: LogFn 6 | info: LogFn 7 | debug: LogFn 8 | } 9 | type LogFn = (logMessage: string) => void 10 | 11 | /** 12 | * Simple logger for ZBClient 13 | */ 14 | const logger = (loglevel: Loglevel): LogFn => (logMessage: string): void => { 15 | let message: string 16 | try { 17 | const parsedMessage = JSON.parse(logMessage) 18 | const gRPC = 19 | parsedMessage.id === 'gRPC Channel' ? ' [gRPC Channel]:' : '' 20 | const taskType = parsedMessage.taskType 21 | ? ` [${parsedMessage.taskType}]` 22 | : '' 23 | const msg = 24 | typeof parsedMessage.message === 'object' 25 | ? JSON.stringify(parsedMessage.message) 26 | : parsedMessage.message 27 | message = `| zeebe | ${gRPC}${taskType} ${loglevel}: ${msg}` 28 | } catch (e: any) { 29 | message = logMessage 30 | } 31 | const time = dayjs().format('HH:mm:ss.SSS') 32 | const err = new Error('Debug stack trace') 33 | const stack = err.stack!.split('\n') 34 | 35 | // tslint:disable: no-console 36 | const loggers = { 37 | DEBUG: console.info, 38 | ERROR: console.error, 39 | INFO: console.info, 40 | } 41 | const logMethod = loggers[loglevel] 42 | const info = 43 | loglevel === 'DEBUG' 44 | ? `${time} ${message}\n${stack}` 45 | : `${time} ${message}` 46 | logMethod(info) 47 | } 48 | 49 | export const ZBSimpleLogger: Logger = { 50 | debug: logger('DEBUG'), 51 | error: logger('ERROR'), 52 | info: logger('INFO'), 53 | } 54 | -------------------------------------------------------------------------------- /src/lib/StatefulLogInterceptor.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import { Characteristics, State } from './ConnectionFactory' 3 | import { ZBLoggerConfig } from './interfaces-1.0' 4 | import { ZBLogger } from './ZBLogger' 5 | 6 | export class StatefulLogInterceptor { 7 | public characteristics: Characteristics 8 | public log: ZBLogger 9 | public blocking: boolean 10 | public state: State = 'ERROR' 11 | public errors = [] 12 | public logs = [] 13 | public initialConnection: boolean 14 | private blockingTimer?: NodeJS.Timeout 15 | constructor({ 16 | characteristics, 17 | logConfig, 18 | }: { 19 | characteristics: Characteristics 20 | logConfig: ZBLoggerConfig 21 | }) { 22 | this.characteristics = characteristics 23 | this.log = new ZBLogger(logConfig) 24 | this.initialConnection = false 25 | this.blocking = 26 | characteristics.startupTime > 0 && this.log.loglevel !== 'DEBUG' 27 | if (this.blocking) { 28 | this.logDirect( 29 | chalk.yellowBright( 30 | 'Authenticating client with Camunda Cloud...' 31 | ) 32 | ) 33 | this.blockingTimer = setTimeout(() => { 34 | if (!this.blocking) { 35 | return 36 | } 37 | this.blocking = false 38 | return this.state === 'ERROR' 39 | ? this.emptyErrors() 40 | : this.emptyLogs() 41 | }, this.characteristics.startupTime) 42 | } 43 | } 44 | 45 | public close() { 46 | if (this.blockingTimer) { 47 | clearTimeout(this.blockingTimer) 48 | } 49 | } 50 | 51 | public logError = err => this.error(err) 52 | public logInfo = msg => this.info(msg) 53 | public logDebug = (msg, ...args) => this.log.debug(msg, ...args) 54 | public logDirect = msg => this.log._tag === 'ZBCLIENT' && this.log.info(msg) 55 | public connectionError = () => { 56 | this.state = 'ERROR' 57 | } 58 | public ready = () => { 59 | this.state = 'CONNECTED' 60 | if (this.blocking) { 61 | this.blocking = false 62 | this.emptyLogs() 63 | } 64 | } 65 | private emptyErrors() { 66 | if (this.errors.length === 0) { 67 | return 68 | } 69 | this.errors.forEach(err => this.logError(err)) 70 | this.logDirect(chalk.redBright('Error connecting to Camunda Cloud.')) 71 | this.errors = [] 72 | } 73 | private emptyLogs() { 74 | if (!this.initialConnection) { 75 | this.initialConnection = true 76 | this.logDirect( 77 | chalk.greenBright( 78 | 'Established encrypted connection to Camunda Cloud.' 79 | ) 80 | ) 81 | } 82 | if (this.logs.length === 0) { 83 | return 84 | } 85 | this.logs.forEach(msg => this.logInfo(msg)) 86 | this.logs = [] 87 | } 88 | private wrap = (store: string[]) => ( 89 | logmethod: (msg: any, ...optionalParameters: any[]) => void 90 | ) => (msg: string) => { 91 | if (this.blocking && this.state === 'ERROR') { 92 | store.push(msg) 93 | return 94 | } 95 | logmethod(msg) 96 | } 97 | // tslint:disable-next-line: member-ordering 98 | private info = this.wrap(this.logs)(m => this.log.info(m)) 99 | 100 | // tslint:disable-next-line: member-ordering 101 | private error = this.wrap(this.errors)(e => this.log.error(e)) 102 | } 103 | -------------------------------------------------------------------------------- /src/lib/TypedEmitter.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | 3 | type EventMap = Record 4 | 5 | type EventKey = string & keyof T 6 | type EventReceiver = () => void 7 | 8 | interface Emitter { 9 | on>(eventName: K, fn: EventReceiver): void 10 | off>(eventName: K, fn: EventReceiver): void 11 | emit>(eventName: K, params?: T[K]): void 12 | } 13 | 14 | export class TypedEmitter implements Emitter { 15 | private emitter = new EventEmitter() 16 | public on>(eventName: K, fn: EventReceiver) { 17 | this.emitter.on(eventName, fn) 18 | return this 19 | } 20 | 21 | public off>(eventName: K, fn: EventReceiver) { 22 | this.emitter.off(eventName, fn) 23 | } 24 | 25 | public emit>(eventName: K, params?: T[K]) { 26 | this.emitter.emit(eventName, params) 27 | } 28 | 29 | public removeAllListeners() { 30 | this.emitter.removeAllListeners() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/ZBJsonLogger.ts: -------------------------------------------------------------------------------- 1 | export const ZBJsonLogger = { 2 | // tslint:disable-next-line: no-console 3 | error: console.error, 4 | // tslint:disable-next-line: no-console 5 | info: console.log, 6 | // tslint:disable-next-line: no-console 7 | debug: console.log, 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/ZBLogger.ts: -------------------------------------------------------------------------------- 1 | import { Chalk } from 'chalk' 2 | import dayjs from 'dayjs' 3 | import * as stackTrace from 'stack-trace' 4 | import { Duration, MaybeTimeDuration } from 'typed-duration' 5 | import { ConfigurationHydrator } from './ConfigurationHydrator' 6 | import { ZBLoggerConfig } from './interfaces-1.0' 7 | import { Loglevel } from './interfaces-published-contract' 8 | 9 | export class ZBLogger { 10 | // tslint:disable-next-line: variable-name 11 | public _tag: 'ZBCLIENT' | 'ZBWORKER' 12 | public loglevel: Loglevel 13 | private colorFn: Chalk 14 | private taskType?: string 15 | private id?: string 16 | private stdout: any 17 | private colorise: boolean 18 | private pollInterval?: MaybeTimeDuration 19 | private namespace: string | undefined 20 | 21 | constructor({ 22 | loglevel, 23 | color, 24 | id, 25 | namespace, 26 | stdout, 27 | taskType, 28 | colorise, 29 | pollInterval, 30 | _tag, 31 | }: ZBLoggerConfig) { 32 | this._tag = _tag 33 | this.colorFn = color || ((m => m) as any) 34 | this.taskType = taskType 35 | this.id = id 36 | if (Array.isArray(namespace)) { 37 | namespace = namespace.join(' ') 38 | } 39 | this.namespace = namespace 40 | this.loglevel = 41 | ConfigurationHydrator.getLogLevelFromEnv() || loglevel || 'INFO' 42 | this.stdout = stdout || console 43 | this.colorise = colorise !== false 44 | this.pollInterval = pollInterval 45 | ? Duration.milliseconds.from(pollInterval) 46 | : pollInterval 47 | } 48 | 49 | public info(message: any, ...optionalParameters) { 50 | if (this.loglevel === 'NONE' || this.loglevel === 'ERROR') { 51 | return 52 | } 53 | const frame = stackTrace.get()[1] 54 | const msg = 55 | optionalParameters.length > 0 56 | ? this.makeMessage(frame, 30, message, optionalParameters) 57 | : this.makeMessage(frame, 30, message) 58 | this.stdout.info(msg) 59 | } 60 | 61 | public error(message, ...optionalParameters: any[]) { 62 | if (this.loglevel === 'NONE') { 63 | return 64 | } 65 | const frame = stackTrace.get()[1] 66 | 67 | const msg = 68 | optionalParameters.length > 0 69 | ? this.makeMessage(frame, 50, message, optionalParameters) 70 | : this.makeMessage(frame, 50, message) 71 | this.stdout.error(msg) 72 | } 73 | 74 | public debug(message: any, ...optionalParameters: any[]) { 75 | if (this.loglevel !== 'DEBUG') { 76 | return 77 | } 78 | const frame = stackTrace.get()[1] 79 | 80 | const msg = 81 | optionalParameters.length > 0 82 | ? this.makeMessage(frame, 20, message, optionalParameters) 83 | : this.makeMessage(frame, 20, message) 84 | if (this.stdout === console) { 85 | this.stdout.info(this._colorise(msg)) 86 | } else { 87 | this.stdout.info(msg) 88 | } 89 | } 90 | 91 | public log(message: any, ...optionalParameters: any[]) { 92 | if (this.loglevel === 'NONE' || this.loglevel === 'ERROR') { 93 | return 94 | } 95 | const frame = stackTrace.get()[1] 96 | 97 | const msg = 98 | optionalParameters.length > 0 99 | ? this.makeMessage(frame, 30, message, optionalParameters) 100 | : this.makeMessage(frame, 30, message) 101 | this.stdout.info(msg) 102 | } 103 | 104 | private makeMessage( 105 | frame: stackTrace.StackFrame, 106 | level: number, 107 | message, 108 | ...optionalParameters 109 | ) { 110 | // tslint:disable: object-literal-sort-keys 111 | const msg: any = { 112 | timestamp: new Date(), 113 | context: `${frame.getFileName()}:${frame.getLineNumber()}`, 114 | id: this.id, 115 | level, 116 | message, 117 | time: dayjs().format('YYYY MMM-DD HH:mm:ssA'), 118 | } 119 | 120 | if (this.pollInterval) { 121 | msg.pollInterval = this.pollInterval 122 | } 123 | if (this.namespace) { 124 | msg.namespace = this.namespace 125 | } 126 | if (this.taskType) { 127 | msg.taskType = this.taskType 128 | } 129 | if (optionalParameters.length > 0) { 130 | ;(msg as any).data = optionalParameters 131 | } 132 | return JSON.stringify(msg) 133 | } 134 | 135 | private _colorise(message: string) { 136 | if (this.colorise) { 137 | // Only colorise console 138 | if (this.colorFn && typeof this.colorFn === 'function') { 139 | return this.colorFn(message) 140 | } else { 141 | return message 142 | } 143 | } 144 | return message 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/lib/ZBWorkerSignature.ts: -------------------------------------------------------------------------------- 1 | import * as ZB from './interfaces-1.0' 2 | import { ZBClientOptions } from './interfaces-published-contract' 3 | 4 | function isConfig( 5 | config: any 6 | ): config is ZB.ZBBatchWorkerConfig { 7 | return typeof config === 'object' 8 | } 9 | 10 | const cleanEmpty = obj => 11 | Object.entries(obj) 12 | .map(([k, v]) => [ 13 | k, 14 | v && typeof v === 'object' 15 | ? !Array.isArray(v) 16 | ? cleanEmpty(v) 17 | : v 18 | : v, 19 | ]) 20 | .reduce((a, [k, v]) => (v == null ? a : { ...a, [k]: v }), {}) 21 | 22 | export function decodeCreateZBWorkerSig< 23 | WorkerInputVariables, 24 | CustomHeaderShape, 25 | WorkerOutputVariables 26 | >(config) { 27 | const coerceConf = config.idOrTaskTypeOrConfig 28 | const conf = isConfig(coerceConf) ? coerceConf : undefined 29 | if (conf) { 30 | return cleanEmpty({ 31 | id: conf.id, 32 | onConnectionError: conf.onConnectionError, 33 | onReady: conf.onReady, 34 | options: { 35 | ...conf, 36 | onReady: undefined, 37 | taskHandler: undefined, 38 | taskType: undefined, 39 | }, 40 | taskHandler: conf.taskHandler, 41 | taskType: conf.taskType, 42 | }) 43 | } 44 | const isShorthandSig = typeof config.taskTypeOrTaskHandler === 'function' 45 | const taskHandler: ZB.ZBWorkerTaskHandler< 46 | WorkerInputVariables, 47 | CustomHeaderShape, 48 | WorkerOutputVariables 49 | > = isShorthandSig 50 | ? (config.taskTypeOrTaskHandler as ZB.ZBWorkerTaskHandler< 51 | WorkerInputVariables, 52 | CustomHeaderShape, 53 | WorkerOutputVariables 54 | >) 55 | : (config.taskHandlerOrOptions as ZB.ZBWorkerTaskHandler< 56 | WorkerInputVariables, 57 | CustomHeaderShape, 58 | WorkerOutputVariables 59 | >) 60 | const id: string | null = isShorthandSig 61 | ? (config.idOrTaskTypeOrConfig as string) 62 | : null 63 | const taskType: string = isShorthandSig 64 | ? (config.idOrTaskTypeOrConfig as string) 65 | : (config.taskTypeOrTaskHandler as string) 66 | const options: ZB.ZBWorkerOptions & ZBClientOptions = 67 | (isShorthandSig 68 | ? config.taskHandlerOrOptions 69 | : config.optionsOrOnConnectionError) || {} 70 | const onConnectionError = isShorthandSig 71 | ? config.optionsOrOnConnectionError 72 | : config.onConnectionError || 73 | options.onConnectionError || 74 | config.onConnectionError 75 | const onReady = options.onReady 76 | return cleanEmpty({ 77 | id, 78 | onConnectionError, 79 | onReady, 80 | options, 81 | taskHandler, 82 | taskType, 83 | }) 84 | } 85 | -------------------------------------------------------------------------------- /src/lib/cancelProcesses.ts: -------------------------------------------------------------------------------- 1 | import { OperateApiClient } from 'operate-api-client' 2 | 3 | const operate = createClient() 4 | 5 | export async function cancelProcesses(processDefinitionKey: string) { 6 | if (!operate) { return } 7 | const processes = await operate.searchProcessInstances({ 8 | filter: { 9 | processDefinitionKey: +processDefinitionKey 10 | } 11 | }) 12 | await Promise.all(processes.items.map(item => 13 | operate.deleteProcessInstance(+item.bpmnProcessId) 14 | )) 15 | } 16 | 17 | 18 | function createClient() { 19 | try { 20 | return new OperateApiClient() 21 | } catch (e: any) { 22 | // console.log(e.message) 23 | // console.log(`Running without access to Operate`) 24 | return null 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/createUniqueTaskType.ts: -------------------------------------------------------------------------------- 1 | // import { readFileSync } from 'fs' 2 | // import { v4 as uuid } from 'uuid' 3 | // // Replace a tasktype in a bpmn model with a unique tasktype 4 | // // This deals with stateful tests 5 | // export function createUniqueTaskType({ 6 | // bpmnFilePath, 7 | // taskTypes, 8 | // messages, 9 | // }: { 10 | // bpmnFilePath: string 11 | // taskTypes: string[] 12 | // messages: string[] 13 | // }): { 14 | // bpmn: Buffer 15 | // taskTypes: { [key: string]: string } 16 | // messages: { [key: string]: string } 17 | // processId: string 18 | // } { 19 | // const bpmn = readFileSync(bpmnFilePath, 'utf8') 20 | // const newTaskTypes = taskTypes.map(t => ({ [t]: uuid() })) 21 | // const newMessages = messages.map(m => ({ [m]: uuid() })) 22 | 23 | // const replacedTasks = 24 | // newTaskTypes.length > 0 25 | // ? newTaskTypes.reduce( 26 | // (p, c) => 27 | // p 28 | // .split( 29 | // ` p.split(Object.keys(c)[0]).join(c[Object.keys(c)[0]]), 45 | // replacedTasks 46 | // ) 47 | // : replacedTasks 48 | 49 | // const processIdPieces = replacedMessages.split(' extends ZBWorkerBase { 14 | private jobBatchMaxTime: number 15 | private jobBuffer: { batch: (job: any) => void } 16 | constructor( 17 | config: ZBBatchWorkerConstructorConfig< 18 | InputVariables, 19 | Headers, 20 | OutputVariables 21 | > 22 | ) { 23 | super(config) 24 | this.jobBatchMaxTime = config.options.jobBatchMaxTime 25 | if (this.timeout < this.jobBatchMaxTime) { 26 | const jobBatchMaxTimeout = this.jobBatchMaxTime 27 | this.log(`\n`) 28 | this.log(chalk.redBright(`=================================`)) 29 | this.log( 30 | `${chalk.yellowBright('WARNING:')} The ${ 31 | this.taskType 32 | } batch worker is ${chalk.yellowBright('MISCONFIGURED')}.` 33 | ) 34 | this.log( 35 | `Its settings can ${chalk.yellowBright( 36 | 'RESULT IN ITS JOBS TIMING OUT' 37 | )}.` 38 | ) 39 | this.log( 40 | `The ${chalk.greenBright( 41 | 'jobBatchMaxTimeout' 42 | )}: ${jobBatchMaxTimeout} is longer than the ${chalk.greenBright( 43 | 'timeout' 44 | )}: ${this.timeout}.` 45 | ) 46 | this.log( 47 | `This can lead to jobs timing out and retried by the broker before this worker executes their batch.` 48 | ) 49 | this.log( 50 | chalk.redBright( 51 | 'This is probably not the behaviour you want. Read the docs, and reconsider your life choices.' 52 | ) 53 | ) 54 | this.log( 55 | chalk.yellowBright( 56 | `Recommended: Reconfigure the ZBBatchWorker to have a higher timeout than its jobBatchMaxTimeout setting.` 57 | ) 58 | ) 59 | this.log(chalk.redBright(`=================================`)) 60 | this.log(`\n`) 61 | } 62 | this.jobBuffer = new JobBatcher({ 63 | batchSize: this.jobBatchMinSize, 64 | handler: this.taskHandler as ZB.ZBBatchWorkerTaskHandler< 65 | any, 66 | any, 67 | any 68 | >, 69 | timeout: this.jobBatchMaxTime, 70 | worker: this, 71 | }) 72 | } 73 | 74 | protected async handleJobs(jobs: ZB.Job[]) { 75 | const batchedJobs = jobs.map( 76 | (job): ZB.BatchedJob => ({ 77 | ...job, 78 | ...this.makeCompleteHandlers(job), 79 | }) 80 | ) 81 | this.jobBuffer.batch(batchedJobs) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/zb/ZBWorker.ts: -------------------------------------------------------------------------------- 1 | import * as ZB from '../lib/interfaces-1.0' 2 | 3 | import { ZBWorkerBase, ZBWorkerConstructorConfig } from '../lib/ZBWorkerBase' 4 | 5 | export class ZBWorker< 6 | WorkerInputVariables, 7 | CustomHeaderShape, 8 | WorkerOutputVariables 9 | > extends ZBWorkerBase< 10 | WorkerInputVariables, 11 | CustomHeaderShape, 12 | WorkerOutputVariables 13 | > { 14 | constructor( 15 | config: ZBWorkerConstructorConfig< 16 | WorkerInputVariables, 17 | CustomHeaderShape, 18 | WorkerOutputVariables 19 | > 20 | ) { 21 | super(config) 22 | } 23 | 24 | protected handleJobs( 25 | jobs: ZB.Job[] 26 | ) { 27 | // Call task handler for each new job 28 | jobs.forEach(async job => this.handleJob(job)) 29 | } 30 | 31 | protected async handleJob( 32 | job: ZB.Job 33 | ) { 34 | try { 35 | /** 36 | * complete.success(variables?: object) and complete.failure(errorMessage: string, retries?: number) 37 | * 38 | * To halt execution of the business process and raise an incident in Operate, call 39 | * complete.failure(errorMessage, 0) 40 | */ 41 | 42 | const workerCallback = this.makeCompleteHandlers(job) 43 | 44 | await (this.taskHandler as ZB.ZBWorkerTaskHandler< 45 | WorkerInputVariables, 46 | CustomHeaderShape, 47 | WorkerOutputVariables 48 | >)( 49 | { 50 | ...job, 51 | cancelWorkflow: workerCallback.cancelWorkflow, 52 | complete: workerCallback.complete, 53 | fail: workerCallback.fail, 54 | error: workerCallback.error, 55 | forward: workerCallback.forward, 56 | }, 57 | this 58 | ) 59 | } catch (e: any) { 60 | this.logger.logError( 61 | `Caught an unhandled exception in a task handler for process instance ${job.processInstanceKey}:` 62 | ) 63 | this.logger.logDebug(job) 64 | this.logger.logError(e.message) 65 | if (this.cancelWorkflowOnException) { 66 | const { processInstanceKey } = job 67 | this.logger.logDebug( 68 | `Cancelling process instance ${processInstanceKey}` 69 | ) 70 | try { 71 | await this.zbClient.cancelProcessInstance( 72 | processInstanceKey 73 | ) 74 | } finally { 75 | this.drainOne() 76 | } 77 | } else { 78 | this.logger.logInfo(`Failing job ${job.key}`) 79 | const retries = job.retries - 1 80 | try { 81 | this.zbClient.failJob({ 82 | errorMessage: `Unhandled exception in task handler ${e}`, 83 | jobKey: job.key, 84 | retries, 85 | retryBackOff: 0, 86 | }) 87 | } catch (e: any) { 88 | this.logger.logDebug(e) 89 | } finally { 90 | this.drainOne() 91 | if (retries > 0) { 92 | this.logger.logDebug( 93 | `The Zeebe engine will handle the retry. Retries left: ${retries}` 94 | ) 95 | } else { 96 | this.logger.logDebug('No retries left for this task') 97 | } 98 | } 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true /* Required because referenced by src/tsconfig.json */, 4 | /* Basic Options */ 5 | "target": "es2018" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, 6 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 7 | "types": ["node", "jest"], 8 | "lib": ["es2018"], 9 | "declaration": true /* Generates corresponding '.d.ts' file. */, 10 | "sourceMap": true /* Generates corresponding '.map' file. */, 11 | // "outDir": "dist" /* Redirect output structure to the directory. */, 12 | // "rootDir": "src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 13 | /* Strict Type-Checking Options */ 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": true /* Enable all strict type-checking options. */, 16 | "noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */, 17 | "noImplicitReturns": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "strictNullChecks": true, 21 | "esModuleInterop": true, 22 | "experimentalDecorators": true, 23 | "resolveJsonModule": true, 24 | "plugins": [ 25 | { 26 | "name": "typescript-tslint-plugin" 27 | } 28 | ] 29 | }, 30 | "include": ["./package.json"] 31 | } 32 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:latest", "tslint-config-prettier"], 4 | "jsRules": {}, 5 | "rules": { 6 | "interface-name": false, 7 | "indent": [true, "tabs", 4], 8 | "semicolon": false, 9 | "quotemark": false, 10 | "trailing-comma": false, 11 | "arrow-parens": false, 12 | "no-submodule-imports": false 13 | }, 14 | "rulesDirectory": [], 15 | "linterOptions": { 16 | "exclude": [ 17 | "./node_modules/**/*", 18 | "./dist/**/*", 19 | "./example/**/*", 20 | "./docs/**/*" 21 | ] 22 | } 23 | } 24 | --------------------------------------------------------------------------------