├── .eslintignore ├── test ├── test-file.txt ├── test-file.png ├── package.json ├── scripts │ ├── clearSetup.js │ └── setup.js ├── license-check.js ├── integration.test.js └── test-process.bpmn ├── examples ├── granting-loans │ ├── .gitignore │ ├── assets │ │ └── deploy.gif │ ├── package.json │ ├── README.md │ └── index.js └── order │ ├── assets │ ├── invoice.txt │ └── order.bpmn │ ├── package.json │ ├── README.md │ ├── index.js │ └── package-lock.json ├── NOTICE ├── .npmignore ├── .gitignore ├── docs ├── logger-error.png ├── logger-success.png ├── BasicAuthInterceptor.md ├── logger.md ├── KeycloakAuthInterceptor.md ├── File.md ├── Variables.md ├── handler.md └── Client.md ├── .github ├── pr-badge.yml └── workflows │ ├── close-stale-issues.yml │ ├── main.yml │ └── CI.yml ├── .eslintrc ├── lib ├── __snapshots__ │ ├── KeycloakAuthInterceptor.test.js.snap │ ├── BasicAuthInterceptor.test.js.snap │ ├── File.test.js.snap │ ├── Client.test.js.snap │ ├── logger.test.js.snap │ └── Variables.test.js.snap ├── __internal │ ├── __snapshots__ │ │ └── utils.test.js.snap │ ├── EngineError.js │ ├── EngineError.test.js │ ├── errors.js │ ├── utils.js │ ├── EngineService.js │ ├── EngineService.test.js │ └── utils.test.js ├── BasicAuthInterceptor.js ├── BasicAuthInterceptor.test.js ├── File.js ├── File.test.js ├── Variables.js ├── logger.js ├── TaskService.js ├── KeycloakAuthInterceptor.js ├── Variables.test.js ├── logger.test.js └── KeycloakAuthInterceptor.test.js ├── pom.xml ├── babel.config.cjs ├── index.js ├── __mocks__ └── got.js ├── package.json ├── CHANGELOG.md ├── README.md └── LICENSE /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /test/test-file.txt: -------------------------------------------------------------------------------- 1 | Hellö Wörld! 2 | -------------------------------------------------------------------------------- /examples/granting-loans/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Camunda 2 | Copyright 2018-2025 Camunda Services GmbH 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | __mocks__/ 3 | examples/ 4 | .idea/ 5 | coverage/ 6 | .github/ -------------------------------------------------------------------------------- /examples/order/assets/invoice.txt: -------------------------------------------------------------------------------- 1 | Amount: £10,000 2 | VAT: £2,000 3 | Total: £12,000 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | tmp/ 3 | docs/api/ 4 | dist/ 5 | .idea 6 | *.iml 7 | coverage/ 8 | -------------------------------------------------------------------------------- /test/test-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camunda/camunda-external-task-client-js/HEAD/test/test-file.png -------------------------------------------------------------------------------- /docs/logger-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camunda/camunda-external-task-client-js/HEAD/docs/logger-error.png -------------------------------------------------------------------------------- /docs/logger-success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camunda/camunda-external-task-client-js/HEAD/docs/logger-success.png -------------------------------------------------------------------------------- /.github/pr-badge.yml: -------------------------------------------------------------------------------- 1 | label: "JIRA" 2 | url: "https://app.camunda.com/jira/browse/$issuePrefix" 3 | message: "$issuePrefix" 4 | color: "0052CC" 5 | -------------------------------------------------------------------------------- /examples/granting-loans/assets/deploy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camunda/camunda-external-task-client-js/HEAD/examples/granting-loans/assets/deploy.gif -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:bpmn-io/node", 4 | "plugin:prettier/recommended", 5 | "plugin:camunda-licensed/apache" 6 | ], 7 | "env": { 8 | "node": true, 9 | "jest": true, 10 | "es6": true 11 | } 12 | } -------------------------------------------------------------------------------- /examples/order/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "order", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "type": "module", 6 | "author": "Camunda Services GmbH", 7 | "license": "MIT", 8 | "dependencies": { 9 | "camunda-external-task-client-js": "3.1.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/__snapshots__/KeycloakAuthInterceptor.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`KeycloakAuthInterceptor should add auth token to intercepted config 1`] = ` 4 | { 5 | "headers": { 6 | "Authorization": "Bearer 1234567890", 7 | }, 8 | "key": "some value", 9 | } 10 | `; 11 | -------------------------------------------------------------------------------- /examples/granting-loans/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "granting-loans", 3 | "version": "1.0.0", 4 | "main": "example.js", 5 | "type": "module", 6 | "author": "Camunda Services GmbH", 7 | "license": "Apache License 2.0", 8 | "dependencies": { 9 | "camunda-external-task-client-js": "3.1.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/__snapshots__/BasicAuthInterceptor.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`BasicAuthInterceptor should add basic auth header to intercepted config 1`] = ` 4 | { 5 | "headers": { 6 | "Authorization": "Basic c29tZSB1c2VybmFtZTpzb21lIHBhc3N3b3Jk", 7 | }, 8 | "key": "some value", 9 | } 10 | `; 11 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | org.camunda 5 | camunda-external-task-client-js 6 | 0.0.1-SNAPSHOT 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /lib/__internal/__snapshots__/utils.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`utils deserializeVariable value should be a File instance if type is file 1`] = ` 4 | { 5 | "type": "file", 6 | "value": File { 7 | "__readFile": [Function], 8 | "content": "", 9 | "createTypedValue": [Function], 10 | "engineService": {}, 11 | "filename": "data", 12 | "load": [Function], 13 | "remotePath": "/execution/process_instance_id/localVariables/variable_key/data", 14 | }, 15 | "valueInfo": {}, 16 | } 17 | `; 18 | -------------------------------------------------------------------------------- /.github/workflows/close-stale-issues.yml: -------------------------------------------------------------------------------- 1 | # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. 2 | # 3 | # You can adjust the behavior by modifying this file. 4 | # For more information, see: 5 | # https://github.com/actions/stale 6 | name: 'Close stale issues and PRs' 7 | on: 8 | schedule: 9 | - cron: '0 3 * * 1' 10 | workflow_dispatch: # can be used to trigger the workflow manually 11 | 12 | jobs: 13 | call-reusable-flow: 14 | uses: camunda/automation-platform-github-actions/.github/workflows/close-stale-issues.yml@main 15 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "version": "1.0.0", 4 | "main": "integration.test.js", 5 | "type": "module", 6 | "license": "MIT", 7 | "scripts": { 8 | "pretest": "node scripts/setup", 9 | "test": "NODE_OPTIONS=--experimental-vm-modules jest", 10 | "posttest": "node scripts/clearSetup" 11 | }, 12 | "devDependencies": { 13 | "@jest/globals": "^29.2.2", 14 | "form-data": "^4.0.0", 15 | "got": "^13.0.0", 16 | "jest": "29.3.1", 17 | "run-camunda": "^9.0.0" 18 | }, 19 | "jest": { 20 | "testEnvironment": "node", 21 | "transform": {}, 22 | "moduleNameMapper": { 23 | "#(.*)": "/node_modules/$1" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/order/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | > NodeJS >= v18 is required 3 | 4 | A Process for Handling Orders 5 | 6 | ## Running the example 7 | 8 | 1. First, make sure to have [Camunda](https://docs.camunda.org/manual/latest/installation/) running. 9 | 10 | 2. Download the following [model](assets/order.bpmn) and deploy it using the Camunda Modeler. 11 | 12 | 3. Install Dependencies: 13 | 14 | ```sh 15 | npm install 16 | ``` 17 | 18 | Or: 19 | 20 | ```sh 21 | yarn 22 | ``` 23 | 24 | 4. Run the example: 25 | ```sh 26 | node index.js 27 | ``` 28 | 29 | ### Output 30 | The terminal output should be: 31 | ``` 32 | ✓ subscribed to topic invoiceCreator 33 | ✓ completed task 21d19522-3e4c-11e8-b8df-186590db1cd7 34 | ``` 35 | -------------------------------------------------------------------------------- /lib/__snapshots__/File.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`File constructor should create instance with proper values 1`] = ` 4 | File { 5 | "__readFile": [Function], 6 | "content": "", 7 | "createTypedValue": [Function], 8 | "encoding": "utf-8", 9 | "filename": "somefile", 10 | "load": [Function], 11 | "localPath": "foo", 12 | "mimetype": "application/json", 13 | } 14 | `; 15 | 16 | exports[`File constructor should create instance with proper values when typedValue is provided 1`] = ` 17 | File { 18 | "__readFile": [Function], 19 | "content": "", 20 | "createTypedValue": [Function], 21 | "encoding": "utf-8", 22 | "filename": "somefile", 23 | "load": [Function], 24 | "localPath": "foo", 25 | "mimetype": "application/json", 26 | } 27 | `; 28 | -------------------------------------------------------------------------------- /examples/granting-loans/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | > NodeJS >= v18 is required 3 | 4 | A Process for Granting Loans 5 | 6 | ## Running the example 7 | 8 | 1. First, make sure to have [Camunda](https://docs.camunda.org/manual/latest/installation/) running. 9 | 10 | 2. Download the following [model](assets/loan-process.bpmn) and deploy it using the Camunda Modeler. 11 | 12 | Deploying from Camunda Modeler 13 | 14 | 3. Install Dependencies: 15 | 16 | ```sh 17 | npm install 18 | ``` 19 | 20 | Or: 21 | 22 | ```sh 23 | yarn 24 | ``` 25 | 26 | 4. Run the example: 27 | ```sh 28 | node index.js 29 | ``` 30 | 31 | ### Output 32 | The output should be: 33 | 34 | ``` 35 | ✓ subscribed to topic creditScoreChecker 36 | ✓ completed task 897ce191-2dea-11e8-a9c0-66b11439c29a 37 | ``` 38 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Bump versions 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | oldVersion: 6 | description: 'Old Version (search)' 7 | required: true 8 | default: '2.X.0' 9 | newVersion: 10 | description: 'New Version (replace)' 11 | required: true 12 | default: '2.X.0' 13 | 14 | jobs: 15 | bump-versions: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Bump versions 20 | uses: camunda/bump-versions-action@v1.7 21 | with: 22 | path: "/examples" 23 | files: "*/**/package.json" 24 | sliceVersion: 0 25 | oldVersion: ${{ github.event.inputs.oldVersion }} 26 | newVersion: ${{ github.event.inputs.newVersion }} 27 | github_token: ${{ secrets.GITHUB_TOKEN }} 28 | -------------------------------------------------------------------------------- /test/scripts/clearSetup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. Camunda licenses this file to you under the Apache License, 6 | * Version 2.0; you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import { stopCamunda } from "run-camunda"; 19 | 20 | stopCamunda(); 21 | -------------------------------------------------------------------------------- /docs/BasicAuthInterceptor.md: -------------------------------------------------------------------------------- 1 | # BasicAuthInterceptor 2 | 3 | A BasicAuthInterceptor instance is a simple interceptor that adds basic authentication to all requests. 4 | 5 | ```js 6 | const { 7 | Client, 8 | BasicAuthInterceptor 9 | } = require("camunda-external-task-client-js"); 10 | 11 | const basicAuthentication = new BasicAuthInterceptor({ 12 | username: "demo", 13 | password: "demo" 14 | }); 15 | 16 | const client = new Client({ 17 | baseUrl: "http://localhost:8080/engine-rest", 18 | interceptors: basicAuthentication 19 | }); 20 | ``` 21 | 22 | ## new BasicAuthInterceptor(options) 23 | 24 | Here's a list of the available options: 25 | 26 | | Option | Description | Type | Required | Default | 27 | | -------- | ------------------------------------- | ------ | -------- | ------- | 28 | | username | username used in basic authentication | string | ✓ | | 29 | | password | password used in basic authentication | string | ✓ | | 30 | -------------------------------------------------------------------------------- /babel.config.cjs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. Camunda licenses this file to you under the Apache License, 6 | * Version 2.0; you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | module.exports = { 19 | env: { 20 | test: { 21 | plugins: ["@babel/plugin-transform-modules-commonjs"], 22 | }, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | Build: 5 | strategy: 6 | matrix: 7 | os: [ubuntu-latest] 8 | node-version: [18] 9 | test-dir: [".", "./test"] 10 | 11 | runs-on: ${{ matrix.os }} 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | - name: Use Node.js 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: Cache Node.js modules 21 | uses: actions/cache@v4 22 | with: 23 | # npm cache files are stored in `~/.npm` on Linux/macOS 24 | path: ~/.npm 25 | key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }} 26 | restore-keys: | 27 | ${{ runner.OS }}-node- 28 | ${{ runner.OS }}- 29 | - name: Install dependencies 30 | run: npm ci 31 | - name: Install test dependencies 32 | if: matrix.test-dir == './test' 33 | run: npm ci 34 | working-directory: ./test 35 | - name: Build 36 | run: npm run test 37 | working-directory: ${{ matrix.test-dir }} 38 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. Camunda licenses this file to you under the Apache License, 6 | * Version 2.0; you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import Client from "./lib/Client.js"; 19 | import logger from "./lib/logger.js"; 20 | import BasicAuthInterceptor from "./lib/BasicAuthInterceptor.js"; 21 | import KeycloakAuthInterceptor from "./lib/KeycloakAuthInterceptor.js"; 22 | import Variables from "./lib/Variables.js"; 23 | import File from "./lib/File.js"; 24 | 25 | export { 26 | Client, 27 | logger, 28 | BasicAuthInterceptor, 29 | KeycloakAuthInterceptor, 30 | Variables, 31 | File, 32 | }; 33 | -------------------------------------------------------------------------------- /docs/logger.md: -------------------------------------------------------------------------------- 1 | # logger 2 | 3 | A logger is a simple middleware that logs various events in the client lifecycle. It can be configured using different log-levels. 4 | 5 | ```js 6 | const { Client, logger } = require("camunda-external-task-client-js"); 7 | 8 | const client = new Client({ 9 | use: logger, 10 | baseUrl: "http://localhost:8080/engine-rest" 11 | }); 12 | ``` 13 | 14 | ## `logger.level(logLevel)` 15 | Returns a logger instance with the configured log level. 16 | ```js 17 | const { Client, logger } = require("camunda-external-task-client-js"); 18 | 19 | const client = new Client({ 20 | use: logger.level('debug'), 21 | baseUrl: "http://localhost:8080/engine-rest" 22 | }); 23 | ``` 24 | 25 | The levels correspond to the npm logging levels: 26 | 27 | ```JSON 28 | { 29 | error: 0, 30 | warn: 1, 31 | info: 2, 32 | verbose: 3, 33 | debug: 4, 34 | silly: 5 35 | } 36 | ``` 37 | 38 | If you do not specify a log level, `info` will be used. 39 | 40 | 41 | ## `logger.success(text)` 42 | 43 | Receives a text and produces a success message out of it. 44 | 45 | ```js 46 | console.log(logger.success("This is a success message!")); 47 | ``` 48 | 49 | ![logger.success](./logger-success.png) 50 | 51 | 52 | 53 | ## `logger.error(text)` 54 | Receives a text and produces an error message out of it. 55 | 56 | ```js 57 | console.log(logger.error("This is an error message!")); 58 | ``` 59 | 60 | ![logger.error](./logger-error.png) 61 | -------------------------------------------------------------------------------- /lib/__snapshots__/Client.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Client executeTask should call handler with task and taskService 1`] = ` 4 | [ 5 | {}, 6 | { 7 | "engineService": EngineService { 8 | "baseUrl": "http://localhost:XXXX/engine-rest", 9 | "complete": [Function], 10 | "fetchAndLock": [Function], 11 | "get": [Function], 12 | "handleFailure": [Function], 13 | "interceptors": undefined, 14 | "lock": [Function], 15 | "post": [Function], 16 | "request": [Function], 17 | "workerId": "foobarId", 18 | }, 19 | "processInstanceId": undefined, 20 | "readOnly": true, 21 | }, 22 | ] 23 | `; 24 | 25 | exports[`Client subscribe should call the API with the custom configs 1`] = ` 26 | [ 27 | { 28 | "maxTasks": 3, 29 | "sorting": [ 30 | { 31 | "sortBy": "createTime", 32 | "sortOrder": "asc", 33 | }, 34 | ], 35 | "topics": [ 36 | { 37 | "includeExtensionProperties": true, 38 | "localVariables": true, 39 | "lockDuration": 3000, 40 | "processDefinitionId": "processId", 41 | "processDefinitionIdIn": [ 42 | "processId", 43 | "processId2", 44 | ], 45 | "processDefinitionKey": "processKey", 46 | "processDefinitionKeyIn": [ 47 | "processKey", 48 | "processKey2", 49 | ], 50 | "processDefinitionVersionTag": "versionTag", 51 | "tenantIdIn": [ 52 | "tenantId", 53 | ], 54 | "topicName": "foo", 55 | "variables": [ 56 | "fooVariable", 57 | "barVariable", 58 | ], 59 | "withoutTenantId": true, 60 | }, 61 | ], 62 | "usePriority": true, 63 | }, 64 | ] 65 | `; 66 | -------------------------------------------------------------------------------- /lib/__snapshots__/logger.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`logger events should log complete:error 1`] = `"✖ couldn't complete task some task id, some error"`; 4 | 5 | exports[`logger events should log complete:success 1`] = `"✓ completed task some task id"`; 6 | 7 | exports[`logger events should log extendLock:error 1`] = `"✖ couldn't handle extend lock of task some task id, some error"`; 8 | 9 | exports[`logger events should log extendLock:success 1`] = `"✓ handled extend lock of task some task id"`; 10 | 11 | exports[`logger events should log handleBpmnError:error 1`] = `"✖ couldn't handle BPMN error of task some task id, some error"`; 12 | 13 | exports[`logger events should log handleBpmnError:success 1`] = `"✓ handled BPMN error of task some task id"`; 14 | 15 | exports[`logger events should log handleFailure:error 1`] = `"✖ couldn't handle failure of task some task id, some error"`; 16 | 17 | exports[`logger events should log handleFailure:success 1`] = `"✓ handled failure of task some task id"`; 18 | 19 | exports[`logger events should log poll:error event 1`] = `"✖ polling failed with undefined"`; 20 | 21 | exports[`logger events should log poll:start event 1`] = `"polling"`; 22 | 23 | exports[`logger events should log poll:stop event 1`] = `"✖ polling stopped"`; 24 | 25 | exports[`logger events should log poll:success event 1`] = `"✓ polled 2 tasks"`; 26 | 27 | exports[`logger events should log subscribe event 1`] = `"✓ subscribed to topic some topic"`; 28 | 29 | exports[`logger events should log unlock:error 1`] = `"✖ couldn't unlock task some task id, some error"`; 30 | 31 | exports[`logger events should log unlock:success 1`] = `"✓ unlocked task some task id"`; 32 | 33 | exports[`logger events should log unsubscribe event 1`] = `"✓ unsubscribed from topic some topic"`; 34 | -------------------------------------------------------------------------------- /lib/BasicAuthInterceptor.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. Camunda licenses this file to you under the Apache License, 6 | * Version 2.0; you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import { MISSING_BASIC_AUTH_PARAMS } from "./__internal/errors.js"; 19 | 20 | class BasicAuthInterceptor { 21 | /** 22 | * @throws Error 23 | */ 24 | constructor(options) { 25 | if (!options || !options.username || !options.password) { 26 | throw new Error(MISSING_BASIC_AUTH_PARAMS); 27 | } 28 | 29 | /** 30 | * Bind member methods 31 | */ 32 | this.getHeader = this.getHeader.bind(this); 33 | this.interceptor = this.interceptor.bind(this); 34 | 35 | this.header = this.getHeader(options); 36 | 37 | return this.interceptor; 38 | } 39 | 40 | getHeader({ username, password }) { 41 | const encoded = Buffer.from(`${username}:${password}`).toString("base64"); 42 | return { Authorization: `Basic ${encoded}` }; 43 | } 44 | 45 | interceptor(config) { 46 | return { ...config, headers: { ...config.headers, ...this.header } }; 47 | } 48 | } 49 | 50 | export default BasicAuthInterceptor; 51 | -------------------------------------------------------------------------------- /lib/BasicAuthInterceptor.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. Camunda licenses this file to you under the Apache License, 6 | * Version 2.0; you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import BasicAuthInterceptor from "./BasicAuthInterceptor.js"; 19 | import { MISSING_BASIC_AUTH_PARAMS } from "./__internal/errors.js"; 20 | 21 | describe("BasicAuthInterceptor", () => { 22 | test("should throw error if username or password are missing", () => { 23 | expect(() => new BasicAuthInterceptor()).toThrowError( 24 | MISSING_BASIC_AUTH_PARAMS 25 | ); 26 | expect( 27 | () => new BasicAuthInterceptor({ username: "some username" }) 28 | ).toThrowError(MISSING_BASIC_AUTH_PARAMS); 29 | expect( 30 | () => new BasicAuthInterceptor({ password: "some password" }) 31 | ).toThrowError(MISSING_BASIC_AUTH_PARAMS); 32 | }); 33 | 34 | test("should add basic auth header to intercepted config", () => { 35 | // given 36 | const basicAuthInterceptor = new BasicAuthInterceptor({ 37 | username: "some username", 38 | password: "some password", 39 | }); 40 | const config = { key: "some value" }; 41 | const headers = basicAuthInterceptor(config); 42 | 43 | // then 44 | expect(headers).toMatchSnapshot(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /__mocks__/got.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. Camunda licenses this file to you under the Apache License, 6 | * Version 2.0; you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import { jest } from "@jest/globals"; 19 | // There is no alternative of `requireActual` for esm as of now 20 | // See `jest.importActual` progress in https://github.com/facebook/jest/pull/10976. 21 | // const got = jest.requireActual("got"); 22 | // const got = jest.createMockFromModule("got"); 23 | 24 | const handleRequest = (url, { testResult }) => { 25 | if (testResult instanceof Error) { 26 | return Promise.reject(testResult); 27 | } 28 | return { 29 | body: 30 | testResult instanceof Object 31 | ? JSON.stringify(testResult) 32 | : Buffer.from(testResult || "", "utf-8"), 33 | headers: { 34 | "content-type": 35 | testResult instanceof Object 36 | ? "application/json" 37 | : "application/octet-stream", 38 | }, 39 | // headers: {}, 40 | }; 41 | }; 42 | 43 | const gotMock = handleRequest; 44 | gotMock.json = () => {}; 45 | 46 | const myModule = jest.fn().mockImplementation(gotMock); 47 | 48 | class HTTPError extends Error {} 49 | class RequestError extends Error {} 50 | myModule.HTTPError = HTTPError; 51 | myModule.RequestError = RequestError; 52 | 53 | export default myModule; 54 | export { myModule as got, HTTPError, RequestError }; 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "3.1.1-SNAPSHOT", 3 | "name": "camunda-external-task-client-js", 4 | "exports": "./index.js", 5 | "type": "module", 6 | "repository": "https://github.com/camunda/camunda-external-task-client-js.git", 7 | "author": "Camunda Services GmbH", 8 | "license": "Apache-2.0", 9 | "scripts": { 10 | "pretest": "node_modules/.bin/eslint lib/ examples/ index.js", 11 | "test": "jest lib --verbose --coverage --no-color && node test/license-check.js", 12 | "test:watch": "jest lib --watch --verbose --coverage" 13 | }, 14 | "devDependencies": { 15 | "@babel/core": "^7.23.9", 16 | "@babel/plugin-transform-modules-commonjs": "^7.23.3", 17 | "@babel/preset-env": "^7.23.9", 18 | "@jest/globals": "^29.7.0", 19 | "babel-jest": "^29.7.0", 20 | "eslint": "^8.56.0", 21 | "eslint-config-prettier": "^9.1.0", 22 | "eslint-plugin-bpmn-io": "1.0.0", 23 | "eslint-plugin-camunda-licensed": "0.4.6", 24 | "eslint-plugin-jest": "^27.6.3", 25 | "eslint-plugin-prettier": "^4.2.1", 26 | "form-data": "^4.0.0", 27 | "jest": "29.7.0", 28 | "license-checker": "^25.0.1", 29 | "prettier": "^2.7.1" 30 | }, 31 | "dependencies": { 32 | "chalk": "^5.3.0", 33 | "got": "^14.2.0" 34 | }, 35 | "description": "Implement your [BPMN Service Task](https://docs.camunda.org/manual/latest/user-guide/process-engine/external-tasks/) in NodeJS.", 36 | "bugs": { 37 | "url": "https://github.com/camunda/camunda-external-task-client-js/issues" 38 | }, 39 | "homepage": "https://github.com/camunda/camunda-external-task-client-js#readme", 40 | "directories": { 41 | "example": "examples", 42 | "lib": "lib", 43 | "test": "test" 44 | }, 45 | "jest": { 46 | "testEnvironment": "node", 47 | "transform": { 48 | "^.+\\.[t|j]sx?$": "babel-jest" 49 | }, 50 | "transformIgnorePatterns": [ 51 | "/node_modules/(?!(got|chalk)).+\\.js$" 52 | ], 53 | "moduleNameMapper": { 54 | "#(.*)": "/node_modules/$1" 55 | }, 56 | "coveragePathIgnorePatterns": [ 57 | "/node_modules/" 58 | ] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/license-check.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. Camunda licenses this file to you under the Apache License, 6 | * Version 2.0; you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import checker from 'license-checker'; 19 | 20 | const ALLOWED_LICENSES = [ 21 | 'MIT', 22 | 'MIT*', 23 | 'ISC', 24 | 'BSD', 25 | 'BSD-2-Clause', 26 | 'BSD-3-Clause', 27 | 'Apache-2.0', 28 | 'Apache-2.0 WITH LLVM-exception', 29 | '(MIT OR CC0-1.0)' 30 | ]; 31 | 32 | console.log( 33 | `\n\nChecking licenses...` 34 | ); 35 | 36 | checker.init( 37 | { 38 | start: '.', 39 | production: true, 40 | excludePrivatePackages: true 41 | }, 42 | function(err, packages) { 43 | if (err) { 44 | throw err; 45 | } else { 46 | const entries = Object.entries(packages); 47 | let licenseWarning = ''; 48 | 49 | for (const [p, info] of entries) { 50 | const licenses = 51 | typeof info.licenses === 'object' 52 | ? info.licenses 53 | : [info.licenses]; 54 | 55 | licenses.forEach(license => { 56 | if (!ALLOWED_LICENSES.includes(license)) { 57 | licenseWarning += `${p} uses ${license}\n`; 58 | } 59 | }); 60 | } 61 | 62 | if (licenseWarning) { 63 | console.error( 64 | `These Packages use unknown licenses:\n${licenseWarning}` 65 | ); 66 | process.exit(1); 67 | } 68 | console.log( 69 | `all good!` 70 | ); 71 | process.exit(0); 72 | } 73 | } 74 | ); 75 | -------------------------------------------------------------------------------- /test/scripts/setup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. Camunda licenses this file to you under the Apache License, 6 | * Version 2.0; you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import fs from "fs"; 19 | import path from "path"; 20 | import got from "got"; 21 | import FormData from "form-data"; 22 | import { startCamunda } from "run-camunda"; 23 | 24 | const deploy = async filePath => { 25 | // constants 26 | const DEPLOYMENT_NAME = "TEST_PROCESS_LOANS"; 27 | const URL = "http://localhost:8080/engine-rest/deployment/create"; 28 | 29 | // create form and deploy 30 | const form = new FormData(); 31 | form.append("deployment-name", DEPLOYMENT_NAME); 32 | form.append(path.basename(filePath), fs.createReadStream(filePath)); 33 | try { 34 | await got.post(URL, { body: form }); 35 | } catch (e) { 36 | throw e.response ? e.response.body.message : e; 37 | } 38 | }; 39 | 40 | const startProcess = async definitionKey => { 41 | try { 42 | await got.post( 43 | `http://localhost:8080/engine-rest/process-definition/key/${definitionKey}/start`, 44 | { json: {} } 45 | ); 46 | } catch (e) { 47 | throw e.response ? e.response.body : e; 48 | } 49 | }; 50 | 51 | const setup = async () => { 52 | await startCamunda(); 53 | console.log("deploying process ..."); 54 | await deploy("./test-process.bpmn"); 55 | console.log("process deployed"); 56 | console.log("starting process ..."); 57 | await startProcess("loan_process"); 58 | console.log("process started"); 59 | }; 60 | 61 | setup(); 62 | -------------------------------------------------------------------------------- /examples/order/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. Camunda licenses this file to you under the Apache License, 6 | * Version 2.0; you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import { 19 | Client, 20 | logger, 21 | Variables, 22 | File, 23 | } from "camunda-external-task-client-js"; 24 | 25 | // configuration for the Client: 26 | // - 'baseUrl': url to the Process Engine 27 | // - 'logger': utility to automatically log important events 28 | const config = { 29 | baseUrl: "http://localhost:8080/engine-rest", 30 | use: logger, 31 | usePriority: false, 32 | sorting: [ 33 | { 34 | sortBy: Client.SortBy.CreateTime, 35 | sortOrder: Client.SortOrder.DESC, 36 | }, 37 | ], 38 | }; 39 | 40 | // create a Client instance with custom configuration 41 | const client = new Client(config); 42 | 43 | // susbscribe to the topic: 'invoiceCreator' 44 | client.subscribe("invoiceCreator", async function ({ task, taskService }) { 45 | // Put your business logic 46 | // complete the task 47 | const date = new Date(); 48 | const invoice = await new File({ localPath: "./assets/invoice.txt" }).load(); 49 | const minute = date.getMinutes(); 50 | const variables = new Variables().setAll({ invoice, date }); 51 | 52 | // check if minute is even 53 | if (minute % 2 === 0) { 54 | // for even minutes, store variables in the process scope 55 | await taskService.complete(task, variables); 56 | } else { 57 | // for odd minutes, store variables in the task local scope 58 | await taskService.complete(task, null, variables); 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /examples/granting-loans/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. Camunda licenses this file to you under the Apache License, 6 | * Version 2.0; you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import { Client, logger, Variables } from "camunda-external-task-client-js"; 19 | 20 | // configuration for the Client: 21 | // - 'baseUrl': url to the Process Engine 22 | // - 'logger': utility to automatically log important events 23 | const config = { 24 | baseUrl: "http://localhost:8080/engine-rest", 25 | use: logger, 26 | }; 27 | 28 | // create a Client instance with custom configuration 29 | const client = new Client(config); 30 | 31 | // create a handler for the task 32 | const handler = async ({ task, taskService }) => { 33 | // get task variable 'defaultScore' 34 | const defaultScore = task.variables.get("defaultScore"); 35 | 36 | // set process variable 'creditScores' 37 | const creditScores = [defaultScore, 9, 1, 4, 10]; 38 | const processVariables = new Variables() 39 | .set("creditScores", creditScores) 40 | .set("bar", new Date()); 41 | 42 | // complete the task 43 | try { 44 | await taskService.complete(task, processVariables); 45 | console.log("I completed my task successfully!!"); 46 | } catch (e) { 47 | console.error(`Failed completing my task, ${e}`); 48 | } 49 | }; 50 | 51 | // susbscribe to the topic 'creditScoreChecker' & provide the created handler 52 | client.subscribe("creditScoreChecker", handler); 53 | 54 | client.subscribe("requestRejecter", async ({ task, taskService }) => { 55 | console.log(task.variables.get("bar")); 56 | console.log(task.variables.get("creditScores")); 57 | }); 58 | -------------------------------------------------------------------------------- /docs/KeycloakAuthInterceptor.md: -------------------------------------------------------------------------------- 1 | # KeycloakAuthInterceptor 2 | 3 | A KeycloakAuthInterceptor instance is an interceptor that adds a Bearer token header, containing 4 | a [Keycloak](https://www.keycloak.org/) access token, to all requests. This interceptor can be used 5 | if the Camunda REST API is protected with [Keycloak Gatekeeper](https://github.com/keycloak/keycloak-gatekeeper). 6 | 7 | This client serves also as an example for OpenID Connect based authentication. It shows also how an interceptor 8 | with async functionality can be implemented with [hooks](https://github.com/sindresorhus/got#hooks), which are 9 | provided by the underlying [got](https://github.com/sindresorhus/got) HTTP request library. 10 | 11 | ```js 12 | const { 13 | Client, 14 | KeycloakAuthInterceptor 15 | } = require("camunda-external-task-client-js"); 16 | 17 | const keycloakAuthentication = new KeycloakAuthInterceptor({ 18 | tokenEndpoint: "https://your.keyclock.domain/realms/your-realm/protocol/openid-connect/token", 19 | clientId: "your-client-id", 20 | clientSecret: "your-client-secret" 21 | }); 22 | 23 | const client = new Client({ 24 | baseUrl: "http://localhost:8080/engine-rest", 25 | interceptors: keycloakAuthentication 26 | }); 27 | ``` 28 | 29 | ## Caching 30 | 31 | The Keycloak access token has an expiry defined. To reduce the requests to the token endpoint, we cache the token 32 | response as long it is valid. 33 | 34 | To poll the API always with a valid token, we subtract the `cacheOffset` from the validity. If the token is 60 seconds 35 | valid and we poll the API every 5 seconds, there could be the case that we poll the API exactly at the time the token 36 | expires. The default `cacheOffset` from 10 seconds is a good balance between the token validity and the average request 37 | duration. 38 | 39 | ## new KeycloakAuthInterceptor(options) 40 | 41 | Here's a list of the available options: 42 | 43 | | Option | Description | Type | Required | Default | 44 | | ------------- | ------------------------------------- | ------ | -------- | ------- | 45 | | tokenEndpoint | URL to the Keycloak token endpoint | string | ✓ | | 46 | | clientId | The Keycloak client id | string | ✓ | | 47 | | clientSecret | The Keycloak client secret | string | ✓ | | 48 | | cacheOffset | The time in seconds to subtract from the token expiry | number | | 10 | 49 | -------------------------------------------------------------------------------- /lib/__internal/EngineError.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. Camunda licenses this file to you under the Apache License, 6 | * Version 2.0; you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import { RequestError } from "got"; 19 | 20 | /** 21 | * An error that contains the error from the Camunda REST API as also the HTTP error details like the status code 22 | * and status message. 23 | * 24 | * If a non 2xx status code occurs, got will throw a `got.HTTPError` that contains the error response from the engine. 25 | * This error isn't extendable, because it sets the error message in the constructor. Therefore we inherit from the 26 | * got base error type (`got.RequestError`), copy the logic form the `got.HTTPError` class and extend it with our API 27 | * response message. 28 | * 29 | * @see https://docs.camunda.org/manual/latest/reference/rest/overview/#error-handling 30 | */ 31 | class EngineError extends RequestError { 32 | constructor(httpError) { 33 | const { response, options } = httpError; 34 | 35 | let responseBody = null; 36 | try { 37 | responseBody = JSON.parse(response.body); 38 | } catch (e) { 39 | responseBody = response.body; 40 | } 41 | 42 | const { message, type, code } = responseBody; 43 | 44 | super( 45 | `Response code ${response.statusCode} (${ 46 | response.statusMessage 47 | }); Error: ${ 48 | message ? message : responseBody 49 | }; Type: ${type}; Code: ${code}`, 50 | {}, 51 | options 52 | ); 53 | this.name = "EngineError"; 54 | 55 | Object.defineProperties(this, { 56 | response: { 57 | value: response, 58 | }, 59 | httpStatusCode: { 60 | value: response.statusCode, 61 | }, 62 | code: { 63 | value: code, 64 | }, 65 | type: { 66 | value: type, 67 | }, 68 | engineMsg: { 69 | value: message, 70 | }, 71 | }); 72 | } 73 | } 74 | 75 | export default EngineError; 76 | -------------------------------------------------------------------------------- /lib/File.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. Camunda licenses this file to you under the Apache License, 6 | * Version 2.0; you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import fs from "fs"; 19 | import util from "util"; 20 | import path from "path"; 21 | 22 | import { MISSING_FILE_OPTIONS } from "./__internal/errors.js"; 23 | 24 | class File { 25 | /** 26 | * @throws Error 27 | * @param options 28 | * @param options.localPath 29 | * @param options.remotePath 30 | * @param options.typedValue 31 | * @param options.filename 32 | * @param options.encoding 33 | * @param options.mimetype 34 | * @param options.engineService 35 | */ 36 | constructor(options = {}) { 37 | this.load = this.load.bind(this); 38 | this.createTypedValue = this.createTypedValue.bind(this); 39 | this.__readFile = util.promisify(fs.readFile); 40 | 41 | const { localPath, remotePath, typedValue } = options; 42 | 43 | if (!localPath && !remotePath && !typedValue) { 44 | throw new Error(MISSING_FILE_OPTIONS); 45 | } 46 | 47 | Object.assign(this, options); 48 | 49 | if (typedValue) { 50 | Object.assign(this, typedValue.valueInfo); 51 | delete this.typedValue; 52 | } 53 | 54 | this.filename = this.filename || path.basename(remotePath || localPath); 55 | this.content = ""; 56 | } 57 | 58 | /** 59 | * Reads file from localPath 60 | * @throws Error 61 | */ 62 | async load() { 63 | // get content either locally or from remotePath 64 | this.content = this.remotePath 65 | ? await this.engineService.get(this.remotePath) 66 | : await this.__readFile(this.localPath); 67 | 68 | return this; 69 | } 70 | 71 | createTypedValue() { 72 | const valueInfo = { filename: this.filename }; 73 | if (this.encoding) { 74 | valueInfo.encoding = this.encoding; 75 | } 76 | if (this.mimetype) { 77 | valueInfo.mimetype = this.mimetype; 78 | } 79 | return { 80 | type: "file", 81 | value: this.content.toString("base64"), 82 | valueInfo, 83 | }; 84 | } 85 | } 86 | 87 | export default File; 88 | -------------------------------------------------------------------------------- /lib/__internal/EngineError.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. Camunda licenses this file to you under the Apache License, 6 | * Version 2.0; you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import got from "got"; 19 | import EngineError from "./EngineError.js"; 20 | 21 | describe("EngineError", () => { 22 | test("construct an error from a Camunda REST API error", () => { 23 | // given 24 | const response = { 25 | body: { 26 | type: "SomeExceptionClass", 27 | message: "a detailed message", 28 | code: 33333, 29 | }, 30 | statusCode: 400, 31 | statusMessage: "Bad request", 32 | }; 33 | const httpError = new got.HTTPError(response); 34 | httpError.response = response; 35 | const expectedPayload = 36 | "Response code 400 (Bad request); Error: a detailed message; Type: SomeExceptionClass; Code: 33333"; 37 | 38 | // when 39 | const engineError = new EngineError(httpError); 40 | 41 | // then 42 | expect(engineError.message).toEqual(expectedPayload); 43 | expect(engineError.engineMsg).toEqual("a detailed message"); 44 | expect(engineError.code).toEqual(33333); 45 | expect(engineError.type).toEqual("SomeExceptionClass"); 46 | expect(engineError.httpStatusCode).toEqual(400); 47 | }); 48 | 49 | test("construct an error with an unexpected response body", () => { 50 | // given 51 | const response = { 52 | body: "Some unexpected error message", 53 | statusCode: 400, 54 | statusMessage: "Bad request", 55 | }; 56 | const httpError = new got.HTTPError(response); 57 | httpError.response = response; 58 | const expectedPayload = 59 | "Response code 400 (Bad request); Error: Some unexpected error message; Type: undefined; Code: undefined"; 60 | 61 | // when 62 | const engineError = new EngineError(httpError); 63 | 64 | // then 65 | expect(engineError.httpStatusCode).toEqual(400); 66 | expect(engineError.message).toEqual(expectedPayload); 67 | expect(engineError.code).toBeUndefined(); 68 | expect(engineError.type).toBeUndefined(); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /docs/File.md: -------------------------------------------------------------------------------- 1 | # File 2 | The File class provides an easy way to create file variables and asynchronously load their content. 3 | 4 | ```js 5 | const { Client, logger, File } = require("camunda-external-task-handler-js"); 6 | 7 | const client = new Client({ baseUrl: "http://localhost:8080/engine-rest" }); 8 | 9 | client.subscribe("foo", async function({ task, taskService}) { 10 | const file = await new File({ localPath: "./data.txt" }).load(); 11 | variables.set("dataFile", file); 12 | }); 13 | ``` 14 | 15 | > **Note:** File variables contents are internally converted to _base64_ strings when completing a task. 16 | > They are also internally parsed to buffers when polling variables. 17 | 18 | 19 | ## `new File(options)` 20 | Here's a list of the available options: 21 | 22 | | Option | Description | Type | Required | Default | 23 | |-----------|----------------------------|--------|----------|--------------------------------------------------------------------------------------------------------------------------| 24 | | localPath | Path used to load the file | string | ✓ | | 25 | | filename | Name of the file | string | | Basename of the localPath. e.g. If the localPath is: `path/to/something.txt`, the filename will then be: `something.txt` | 26 | | encoding | Encoding of the file | string | | | 27 | | mimetype | Mimetype of the file | string | | | | 28 | 29 | 30 | ## `file.load()` 31 | Loads **asynchronously** the file content from the `localPath`. 32 | 33 | > **Note:** `file.load()` returns the File instance. This can be helpful 34 | for chaining calls. 35 | 36 | ## File Properties 37 | | Property | Description | Type | 38 | |------------------|-----------------------------------------|--------| 39 | | `file.content` | Binary content of the file | buffer | 40 | | `file.localPath` | Path that used to load the file | string | 41 | | `file.filename` | Name of the file | string | 42 | | `file.encoding` | Encoding of the file | string | 43 | | `file.mimetype` | Mimetype of the file | string | 44 | -------------------------------------------------------------------------------- /lib/__internal/errors.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. Camunda licenses this file to you under the Apache License, 6 | * Version 2.0; you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | // Client 19 | const MISSING_BASE_URL = 20 | "Couldn't instantiate Client, missing configuration parameter 'baseUrl'"; 21 | const WRONG_INTERCEPTOR = 22 | "Interceptors should be a function or an array of functions"; 23 | const WRONG_MIDDLEWARES = 24 | "Middleware(s) should be a function or an array of functions"; 25 | const ALREADY_REGISTERED = "Subscription failed, already subscribed to topic"; 26 | const MISSING_HANDLER = "Subscription failed, missing handler function"; 27 | 28 | // Task Service 29 | const MISSING_TASK = "Couldn't complete task, task id is missing"; 30 | const MISSING_ERROR_CODE = 31 | "Couldn't throw BPMN Error, no error code was provided"; 32 | const MISSING_DURATION = "Couldn't lock task, no duration was provided"; 33 | const MISSING_NEW_DURATION = 34 | "Couldn't extend lock time, no new duration was provided"; 35 | 36 | // Basic Auth Interceptor 37 | const MISSING_BASIC_AUTH_PARAMS = 38 | "Couldn't instantiate BasicAuthInterceptor, missing configuration parameter " + 39 | "'username' or 'password'"; 40 | 41 | // Keycloak Auth Interceptor 42 | const MISSING_KEYCLOAK_AUTH_PARAMS = 43 | "Couldn't instantiate KeycloakAuthInterceptor, missing configuration parameter " + 44 | "'tokenEndpoint', 'clientId' or 'clientSecret'"; 45 | const UNEXPECTED_KEYCLOAK_TOKEN_RESULT = 46 | "Couldn't get access token from Keycloak provider; got"; 47 | 48 | // FileService 49 | const MISSING_FILE_OPTIONS = 50 | "Couldn't create a File, make sure to provide one of the following" + 51 | " parameters: \n- path \ntypedValue"; 52 | 53 | const WRONG_SORTING = 54 | "Couldn't instantiate Client, 'sorting' parameter should be an array."; 55 | const WRONG_SORTING_SORT_BY = 56 | "Couldn't instantiate Client, wrong 'sorting.sortBy' parameter. Possible values: "; 57 | const WRONG_SORTING_SORT_ORDER = 58 | "Couldn't instantiate Client, wrong 'sorting.sortOrder' parameter. Possible values: "; 59 | 60 | export { 61 | MISSING_BASE_URL, 62 | ALREADY_REGISTERED, 63 | MISSING_HANDLER, 64 | MISSING_TASK, 65 | WRONG_INTERCEPTOR, 66 | MISSING_ERROR_CODE, 67 | MISSING_DURATION, 68 | MISSING_NEW_DURATION, 69 | MISSING_BASIC_AUTH_PARAMS, 70 | MISSING_KEYCLOAK_AUTH_PARAMS, 71 | UNEXPECTED_KEYCLOAK_TOKEN_RESULT, 72 | WRONG_MIDDLEWARES, 73 | MISSING_FILE_OPTIONS, 74 | WRONG_SORTING, 75 | WRONG_SORTING_SORT_BY, 76 | WRONG_SORTING_SORT_ORDER, 77 | }; 78 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 3.1.0 4 | ### Features 5 | 6 | - Update dependencies ([#3698](https://github.com/camunda/camunda-bpm-platform/issues/3698),[#3906](https://github.com/camunda/camunda-bpm-platform/issues/3906)) 7 | - Add sort by create time capability ([#3928](https://github.com/camunda/camunda-bpm-platform/issues/3928)) 8 | 9 | ## 3.0.1 10 | ### Features 11 | - Update got package ([#3135](https://github.com/camunda/camunda-bpm-platform/issues/3135)) 12 | - Rethrow errors when calling TaskService api ([#3222](https://github.com/camunda/camunda-bpm-platform/issues/3222)) 13 | 14 | ## 3.0.0 15 | ### Features 16 | - Bump json5 from 2.2.1 to 2.2.3 ([#3091](https://github.com/camunda/camunda-bpm-platform/issues/3091)) 17 | 18 | ## 3.0.0-alpha1 19 | ### Features 20 | - Add extension property support ([#264](https://github.com/camunda/camunda-external-task-client-js/pull/264)) 21 | - Update dependencies to latest version & update project to ECMAScript modules ([#265](https://github.com/camunda/camunda-external-task-client-js/pull/265)) 22 | - Update path-parse dependency ([#268](https://github.com/camunda/camunda-external-task-client-js/pull/268)) 23 | 24 | ## 2.3.0 25 | ### Features 26 | - Expose exception error code ([#257](https://github.com/camunda/camunda-external-task-client-js/pull/257)) 27 | 28 | ## 2.2.0 29 | ### Features 30 | - Support setting transient variables via API ([#244](https://github.com/camunda/camunda-external-task-client-js/pull/244)) 31 | 32 | ## 2.1.1 33 | ### Bug Fixes 34 | - Fix loading file variables ([#208](https://github.com/camunda/camunda-external-task-client-js/pull/208)) 35 | 36 | ### Dependency Updates 37 | - Bump lodash to version 4.17.21 38 | - Bump ws to 5.2.3 39 | - Bump normalize-url to 4.5.1 40 | - Bump y18n to 3.2.2 41 | 42 | ## 2.1.0 43 | ### Features 44 | - Allow manual locking of a Task 45 | 46 | ## 2.0.0 47 | ### Features 48 | - Support for Keycloak auth secured rest API 49 | 50 | ### Deprecations 51 | - Removed support for Node v8 and v9. Please use node version 10 or higher. 52 | 53 | ## 1.3.1 54 | ### Features 55 | - support localVariables when fetching Tasks 56 | 57 | ### Changes to the logging behavior 58 | - Not every Action will be logged by default. For example, polling will no longer be logged if it is successful. 59 | You can define the log level as described in the [docs](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/logger.md#loggerlevelloglevel). To emulate >=1.3.0 bahaviour, use `logger.level('debug')` 60 | 61 | ## 1.3.0 62 | ### Features 63 | - Use priority when fetching Tasks 64 | - Filter tasks by version tag 65 | 66 | ## 1.2.0 67 | ### Features 68 | - Set maximum number of executed tasks using `maxParallelExecutions` 69 | 70 | ## 1.1.1 71 | ### Features 72 | - Filter tasks by tenant 73 | - Filter tasks by process definition 74 | 75 | ## 1.1.0-alpha1 76 | ### Features 77 | - Make it possible to pass error message and variables when handling a bpmn error. 78 | 79 | ## 1.0.0 80 | ### Features 81 | - Filter tasks by business key 82 | 83 | ### Bug Fixes 84 | - Setting typed date variable with a string value causes serialization issue 85 | 86 | ## 0.2.0 87 | ### Features 88 | - Setting Local Variables 89 | - Support for File & Date Variables 90 | 91 | ## 0.1.1 92 | 93 | ### Features 94 | - Exchange Process Variables 95 | 96 | ## 0.1.0 97 | 98 | ### Features 99 | - Fetch and Lock 100 | - Complete 101 | - Handle Failure 102 | - Handle BPMN Error 103 | - Extend Lock 104 | - Unlock 105 | -------------------------------------------------------------------------------- /test/integration.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. Camunda licenses this file to you under the Apache License, 6 | * Version 2.0; you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import fs from "fs"; 19 | import { Client, logger, Variables, File } from "../index.js"; 20 | 21 | describe("integration", () => { 22 | let client, expectedScore, expectedUser, expectedTextFile, expectedBinaryFile; 23 | 24 | beforeAll(() => { 25 | const config = { 26 | baseUrl: "http://localhost:8080/engine-rest", 27 | use: logger 28 | }; 29 | 30 | expectedScore = 6; 31 | expectedUser = { name: "Jean Pierre", balance: "$2000" }; 32 | expectedTextFile = fs.readFileSync("./test-file.txt").toString("utf-8"); 33 | expectedBinaryFile = fs.readFileSync("./test-file.png"); 34 | // create a Client instance with custom configuration 35 | client = new Client(config); 36 | }); 37 | 38 | afterAll(() => { 39 | client.stop(); 40 | }); 41 | 42 | test("should subscribe client and complete with process variables", () => { 43 | // susbscribe to the topic: 'creditScoreChecker' 44 | return new Promise((resolve, reject) => { 45 | client.subscribe("creditScoreChecker", async function({ 46 | task, 47 | taskService 48 | }) { 49 | try { 50 | const textAttachment = await new File({ 51 | localPath: "./test-file.txt", 52 | encoding: "utf-8", 53 | mimetype: "text/plain", 54 | }).load(); 55 | const binaryAttachment = await new File({ 56 | localPath: "./test-file.png", 57 | encoding: "utf-8", 58 | mimetype: "image/png", 59 | }).load(); 60 | const processVariables = new Variables() 61 | .set("score", expectedScore) 62 | .set("user", expectedUser) 63 | .set("textAttachment", textAttachment) 64 | .set("binaryAttachment", binaryAttachment); 65 | await taskService.complete(task, processVariables); 66 | resolve(); 67 | } catch (e) { 68 | reject(e); 69 | } 70 | }); 71 | }); 72 | }); 73 | 74 | test("should subscribe client, receive process variables and handle failure", () => { 75 | // susbscribe to the topic: 'creditScoreChecker' 76 | return new Promise((resolve, reject) => { 77 | client.subscribe("loanGranter", async function({ task, taskService }) { 78 | try { 79 | const { score, user, textAttachment, binaryAttachment } = task.variables.getAll(); 80 | expect(score).toBe(expectedScore); 81 | expect(user).toEqual(expectedUser); 82 | expect((await textAttachment.load()).content.toString("utf-8")).toEqual(expectedTextFile); 83 | expect((await binaryAttachment.load()).content).toEqual(expectedBinaryFile); 84 | await taskService.handleFailure(task, { 85 | errorMessage: "something failed" 86 | }); 87 | resolve(); 88 | } catch (e) { 89 | reject(e); 90 | } 91 | }); 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /lib/File.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. Camunda licenses this file to you under the Apache License, 6 | * Version 2.0; you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import { jest } from "@jest/globals"; 19 | import { File } from "../index.js"; 20 | import { MISSING_FILE_OPTIONS } from "./__internal/errors.js"; 21 | 22 | describe("File", () => { 23 | describe("constructor", () => { 24 | it("should throw Error when neither localPath nor typedValue are provided", () => { 25 | expect(() => new File()).toThrowError(MISSING_FILE_OPTIONS); 26 | }); 27 | 28 | it("should create instance with proper values when typedValue is provided", () => { 29 | // given 30 | const typedValue = { 31 | valueInfo: { 32 | filename: "somefile", 33 | mimetype: "application/json", 34 | encoding: "utf-8", 35 | }, 36 | }; 37 | const file = new File({ localPath: "foo", typedValue }); 38 | 39 | // then 40 | expect(file).toMatchSnapshot(); 41 | }); 42 | 43 | it("should create instance with proper values", () => { 44 | // given 45 | const options = { 46 | filename: "somefile", 47 | mimetype: "application/json", 48 | encoding: "utf-8", 49 | localPath: "foo", 50 | }; 51 | 52 | const file = new File(options); 53 | 54 | // then 55 | expect(file).toMatchSnapshot(); 56 | }); 57 | }); 58 | 59 | describe("load", () => { 60 | it("should load content from remotePath when it's provided", async () => { 61 | // given 62 | const engineService = { 63 | get: jest 64 | .fn() 65 | .mockImplementation(() => Promise.resolve(Buffer.from("", "utf-8"))), 66 | }; 67 | const remotePath = "some/remote/path"; 68 | const expectedBuffer = Buffer.from(await engineService.get(remotePath)); 69 | const file = await new File({ remotePath, engineService }).load(); 70 | 71 | // then 72 | expect(file.content).toEqual(expectedBuffer); 73 | }); 74 | 75 | it("should load content from localPath when it's provided", async () => { 76 | // given 77 | const localPath = "some/local/path"; 78 | let file = await new File({ localPath }); 79 | file.__readFile = jest.fn().mockImplementation(() => { 80 | return Promise.resolve("some content"); 81 | }); 82 | const expectedContent = "some content"; 83 | file = await file.load(); 84 | const content = file.content; 85 | 86 | // then 87 | expect(content).toBe(expectedContent); 88 | }); 89 | }); 90 | 91 | describe("createTypedValue", () => { 92 | it("should create typedValue with provided parameters", async () => { 93 | // given 94 | const valueInfo = { 95 | filename: "somefile", 96 | mimetype: "application/text", 97 | encoding: "utf-8", 98 | }; 99 | const value = "this some random value"; 100 | const expectedTypedValue = { 101 | value: Buffer.from(value).toString("utf-8"), 102 | type: "file", 103 | valueInfo, 104 | }; 105 | const engineService = { 106 | get: jest.fn().mockImplementation(() => Promise.resolve(value)), 107 | }; 108 | const remotePath = "some/remote/path"; 109 | const file = await new File({ 110 | remotePath, 111 | engineService, 112 | ...valueInfo, 113 | }).load(); 114 | 115 | // then 116 | expect(file.createTypedValue()).toEqual(expectedTypedValue); 117 | }); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /lib/Variables.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. Camunda licenses this file to you under the Apache License, 6 | * Version 2.0; you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import { 19 | getVariableType, 20 | mapEntries, 21 | serializeVariable, 22 | deserializeVariable, 23 | } from "./__internal/utils.js"; 24 | 25 | function Variables(initialVariables = {}, options = {}) { 26 | const { readOnly, processInstanceId, engineService } = options; 27 | 28 | let dirtyVariables = {}; 29 | 30 | /** 31 | * @returns the typedValue corresponding to variableName 32 | * @param variableName 33 | */ 34 | this.getTyped = (variableName) => { 35 | let typedValue = initialVariables[variableName]; 36 | 37 | if (!typedValue) { 38 | return null; 39 | } 40 | 41 | return deserializeVariable({ 42 | key: variableName, 43 | typedValue, 44 | processInstanceId, 45 | engineService, 46 | }); 47 | }; 48 | 49 | /** 50 | * @returns the value corresponding to variableName 51 | * @param variableName 52 | */ 53 | this.get = (variableName) => { 54 | const { value } = { ...this.getTyped(variableName) }; 55 | return value; 56 | }; 57 | 58 | /** 59 | * @returns the values of all variables 60 | */ 61 | this.getAll = () => 62 | mapEntries(initialVariables, ({ key }) => ({ [key]: this.get(key) })); 63 | 64 | /** 65 | * @returns the typed values of all variables 66 | */ 67 | this.getAllTyped = () => 68 | mapEntries(initialVariables, ({ key }) => ({ [key]: this.getTyped(key) })); 69 | 70 | /** 71 | * @returns the dirty variables 72 | */ 73 | this.getDirtyVariables = () => { 74 | return dirtyVariables; 75 | }; 76 | 77 | if (!readOnly) { 78 | /** 79 | * Sets typed value for variable corresponding to variableName 80 | * @param variableName 81 | * @param typedValue 82 | */ 83 | this.setTyped = (variableName, typedValue) => { 84 | initialVariables[variableName] = dirtyVariables[variableName] = 85 | serializeVariable({ 86 | key: variableName, 87 | typedValue, 88 | }); 89 | return this; 90 | }; 91 | 92 | /** 93 | * Sets value for variable corresponding to variableName 94 | * The type is determined automatically 95 | * @param variableName 96 | * @param value 97 | */ 98 | this.set = (variableName, value) => { 99 | const type = getVariableType(value); 100 | return this.setTyped(variableName, { type, value, valueInfo: {} }); 101 | }; 102 | 103 | /** 104 | * Sets value for variable corresponding to variableName 105 | * The type is determined automatically 106 | * The variable has transient flag: true 107 | * @param variableName 108 | * @param value 109 | */ 110 | this.setTransient = (variableName, value) => { 111 | const type = getVariableType(value); 112 | return this.setTyped(variableName, { 113 | type, 114 | value, 115 | valueInfo: { transient: true }, 116 | }); 117 | }; 118 | 119 | /** 120 | * Sets the values of multiple variables at once 121 | * The new values are merged with existing ones 122 | * @param values 123 | */ 124 | this.setAll = (values) => { 125 | const self = this; 126 | Object.entries(values).forEach(([key, value]) => { 127 | self.set(key, value); 128 | }); 129 | return self; 130 | }; 131 | 132 | /** 133 | * Sets the typed values of multiple variables at once 134 | * The new typedValues are merged with existing ones 135 | * @param typedValues 136 | */ 137 | this.setAllTyped = (typedValues) => { 138 | const self = this; 139 | Object.entries(typedValues).forEach(([key, typedValue]) => { 140 | self.setTyped(key, typedValue); 141 | }); 142 | return self; 143 | }; 144 | } 145 | } 146 | 147 | export default Variables; 148 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. Camunda licenses this file to you under the Apache License, 6 | * Version 2.0; you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import chalk from "chalk"; 19 | 20 | /** 21 | * @returns a formatted success message 22 | */ 23 | const success = (message) => `${chalk.green("✓")} ${chalk.green(message)}`; 24 | 25 | /** 26 | * @returns a formatted error message 27 | */ 28 | const error = (message) => `${chalk.red("✖")} ${chalk.red(message)}`; 29 | 30 | const levels = { 31 | error: 0, 32 | warn: 1, 33 | info: 2, 34 | verbose: 3, 35 | debug: 4, 36 | silly: 5, 37 | }; 38 | 39 | /** 40 | * logs various events from client 41 | * @param client 42 | */ 43 | const logger = (client, clientLogLevel) => { 44 | const log = (messageLogLevel, message) => { 45 | if (!message) { 46 | console.log(messageLogLevel); 47 | return; 48 | } 49 | 50 | if (levels[messageLogLevel] <= clientLogLevel) { 51 | console.log(message); 52 | } 53 | }; 54 | 55 | switch (typeof clientLogLevel) { 56 | case "string": 57 | clientLogLevel = levels[clientLogLevel]; 58 | break; 59 | case "number": 60 | break; 61 | default: 62 | clientLogLevel = levels["info"]; 63 | break; 64 | } 65 | 66 | client.on("subscribe", (topic) => { 67 | log("info", success(`subscribed to topic ${topic}`)); 68 | }); 69 | 70 | client.on("unsubscribe", (topic) => { 71 | log("info", success(`unsubscribed from topic ${topic}`)); 72 | }); 73 | 74 | client.on("poll:start", () => { 75 | log("debug", "polling"); 76 | }); 77 | 78 | client.on("poll:stop", () => { 79 | log("debug", error("polling stopped")); 80 | }); 81 | 82 | client.on("poll:success", (tasks) => { 83 | const output = success(`polled ${tasks.length} tasks`); 84 | log("debug", output); 85 | }); 86 | 87 | client.on("poll:error", (e) => { 88 | const output = error(`polling failed with ${e}`); 89 | log("error", output); 90 | }); 91 | 92 | client.on("complete:success", ({ id }) => { 93 | log("info", success(`completed task ${id}`)); 94 | }); 95 | 96 | client.on("complete:error", ({ id }, e) => { 97 | log("error", error(`couldn't complete task ${id}, ${e}`)); 98 | }); 99 | 100 | client.on("handleFailure:success", ({ id }) => { 101 | log("info", success(`handled failure of task ${id}`)); 102 | }); 103 | 104 | client.on("handleFailure:error", ({ id }, e) => { 105 | log("error", error(`couldn't handle failure of task ${id}, ${e}`)); 106 | }); 107 | 108 | client.on("handleBpmnError:success", ({ id }) => { 109 | log("info", success(`handled BPMN error of task ${id}`)); 110 | }); 111 | 112 | client.on("handleBpmnError:error", ({ id }, e) => { 113 | log("error", error(`couldn't handle BPMN error of task ${id}, ${e}`)); 114 | }); 115 | 116 | client.on("extendLock:success", ({ id }) => { 117 | log("info", success(`handled extend lock of task ${id}`)); 118 | }); 119 | 120 | client.on("extendLock:error", ({ id }, e) => { 121 | log("error", error(`couldn't handle extend lock of task ${id}, ${e}`)); 122 | }); 123 | 124 | client.on("unlock:success", ({ id }) => { 125 | log("info", success(`unlocked task ${id}`)); 126 | }); 127 | 128 | client.on("unlock:error", ({ id }, e) => { 129 | log("error", error(`couldn't unlock task ${id}, ${e}`)); 130 | }); 131 | 132 | client.on("lock:success", ({ id }) => { 133 | log("info", success(`locked task ${id}`)); 134 | }); 135 | 136 | client.on("lock:error", ({ id }, e) => { 137 | log("error", error(`couldn't lock task ${id}, ${e}`)); 138 | }); 139 | }; 140 | 141 | /** 142 | * Returns Logger with configured log-level 143 | * @param level 144 | */ 145 | const level = (level) => { 146 | return function (client) { 147 | logger(client, level); 148 | }; 149 | }; 150 | 151 | // export logger & attach to it success and error methods 152 | export default Object.assign(logger, { success, error, level }); 153 | -------------------------------------------------------------------------------- /lib/__internal/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. Camunda licenses this file to you under the Apache License, 6 | * Version 2.0; you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import File from "../File.js"; 19 | 20 | /** 21 | * Checks if parameter is a function 22 | */ 23 | const isFunction = (f) => typeof f === "function"; 24 | 25 | /** 26 | * Applies test function on each element on the array and ANDs the results 27 | * @param [Array] arr 28 | * @param [Function] test 29 | */ 30 | const andArrayWith = (arr, test) => 31 | arr.reduce((boolean, current) => boolean && test(current), true); 32 | 33 | /** 34 | * Checks if parameter is an array of functions 35 | */ 36 | const isArrayOfFunctions = (a) => 37 | Array.isArray(a) && a.length > 0 && andArrayWith(a, isFunction); 38 | 39 | /** 40 | * Checks if parameter is undefined or null 41 | */ 42 | const isUndefinedOrNull = (a) => typeof a === "undefined" || a === null; 43 | 44 | const typeMatchers = { 45 | null: isUndefinedOrNull, 46 | 47 | /** 48 | * @returns {boolean} true if value is Integer 49 | */ 50 | integer(a) { 51 | return ( 52 | Number.isInteger(a) && a >= -Math.pow(2, 31) && a <= Math.pow(2, 31) - 1 53 | ); 54 | }, 55 | 56 | /** 57 | * @returns {boolean} true if value is Long 58 | */ 59 | long(a) { 60 | return Number.isInteger(a) && !typeMatchers.integer(a); 61 | }, 62 | 63 | /** 64 | * @returns {boolean} true if value is Double 65 | */ 66 | double(a) { 67 | return typeof a === "number" && !Number.isInteger(a); 68 | }, 69 | 70 | /** 71 | * @returns {boolean} true if value is Boolean 72 | */ 73 | boolean(a) { 74 | return typeof a === "boolean"; 75 | }, 76 | 77 | /** 78 | * @returns {boolean} true if value is String 79 | */ 80 | string(a) { 81 | return typeof a === "string"; 82 | }, 83 | 84 | /** 85 | * @returns {boolean} true if value is File 86 | */ 87 | file(a) { 88 | return a instanceof File; 89 | }, 90 | 91 | /** 92 | * @returns {boolean} true if value is Date. 93 | * */ 94 | date(a) { 95 | return a instanceof Date; 96 | }, 97 | 98 | /** 99 | * @returns {boolean} true if value is JSON 100 | */ 101 | json(a) { 102 | return typeof a === "object"; 103 | }, 104 | }; 105 | 106 | /** 107 | * @returns the type of the variable 108 | * @param variable: external task variable 109 | */ 110 | const getVariableType = (variable) => { 111 | const match = Object.entries(typeMatchers).filter( 112 | ([matcherKey, matcherFunction]) => matcherFunction(variable) 113 | )[0]; 114 | 115 | return match[0]; 116 | }; 117 | 118 | /** 119 | * @returns object mapped by applying mapper to each of its entries 120 | * @param object 121 | * @param mapper 122 | */ 123 | const mapEntries = (object, mapper) => 124 | Object.entries(object).reduce((accumulator, [key, value]) => { 125 | return { ...accumulator, ...mapper({ key, value }) }; 126 | }, {}); 127 | 128 | const deserializeVariable = ({ 129 | key, 130 | typedValue, 131 | processInstanceId, 132 | engineService, 133 | }) => { 134 | let { value, type } = { ...typedValue }; 135 | 136 | type = type.toLowerCase(); 137 | 138 | if (type === "json") { 139 | value = JSON.parse(value); 140 | } 141 | 142 | if (type === "file") { 143 | let remotePath = `/execution/${processInstanceId}/localVariables/${key}/data`; 144 | value = new File({ typedValue, remotePath, engineService }); 145 | } 146 | 147 | if (type === "date") { 148 | value = new Date(value); 149 | } 150 | 151 | return { ...typedValue, value, type }; 152 | }; 153 | 154 | const serializeVariable = ({ key, typedValue }) => { 155 | let { value, type } = { ...typedValue }; 156 | 157 | type = type.toLowerCase(); 158 | 159 | if (type === "file" && value instanceof File) { 160 | return value.createTypedValue(); 161 | } 162 | 163 | if (type === "json" && typeof value !== "string") { 164 | value = JSON.stringify(value); 165 | } 166 | 167 | if (type === "date" && value instanceof Date) { 168 | value = value.toISOString().replace(/Z$/, "+0000"); 169 | } 170 | 171 | return { ...typedValue, value, type }; 172 | }; 173 | 174 | export { 175 | isFunction, 176 | andArrayWith, 177 | isArrayOfFunctions, 178 | isUndefinedOrNull, 179 | getVariableType, 180 | mapEntries, 181 | serializeVariable, 182 | deserializeVariable, 183 | }; 184 | -------------------------------------------------------------------------------- /lib/__internal/EngineService.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. Camunda licenses this file to you under the Apache License, 6 | * Version 2.0; you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import { got, HTTPError } from "got"; 19 | import EngineError from "./EngineError.js"; 20 | 21 | class EngineService { 22 | constructor({ workerId, baseUrl, interceptors }) { 23 | this.workerId = workerId; 24 | this.baseUrl = `${baseUrl.replace(/\/$/, "")}`; 25 | this.interceptors = interceptors; 26 | 27 | /** 28 | * Bind member methods 29 | */ 30 | this.request = this.request.bind(this); 31 | this.post = this.post.bind(this); 32 | this.get = this.get.bind(this); 33 | this.fetchAndLock = this.fetchAndLock.bind(this); 34 | this.complete = this.complete.bind(this); 35 | this.handleFailure = this.handleFailure.bind(this); 36 | this.lock = this.lock.bind(this); 37 | } 38 | 39 | async request(method, path, options) { 40 | const url = `${this.baseUrl}${path}`; 41 | let newOptions = { method, ...options }; 42 | 43 | if (this.interceptors) { 44 | newOptions = this.interceptors.reduce((config, interceptor) => { 45 | return interceptor(config); 46 | }, newOptions); 47 | } 48 | 49 | try { 50 | const { body, headers } = await got(url, { 51 | ...newOptions, 52 | responseType: "buffer", 53 | }); 54 | if (headers["content-type"] === "application/json") { 55 | return JSON.parse(body.toString("utf-8")); 56 | } else { 57 | return body; 58 | } 59 | } catch (e) { 60 | if (e instanceof HTTPError) { 61 | throw new EngineError(e); 62 | } 63 | 64 | throw e; 65 | } 66 | } 67 | 68 | /** 69 | * @throws HTTPError 70 | * @param path 71 | * @param options 72 | * @returns {Promise} 73 | */ 74 | post(path, options) { 75 | return this.request("POST", path, options); 76 | } 77 | 78 | /** 79 | * @throws HTTPError 80 | * @param path 81 | * @param options 82 | * @returns {Promise} 83 | */ 84 | get(path, options) { 85 | return this.request("GET", path, options); 86 | } 87 | 88 | /** 89 | * @throws HTTPError 90 | * @param requestBody 91 | * @returns {Promise} 92 | */ 93 | fetchAndLock(requestBody) { 94 | return this.post("/external-task/fetchAndLock", { 95 | json: { ...requestBody, workerId: this.workerId }, 96 | }); 97 | } 98 | 99 | /** 100 | * @throws HTTPError 101 | * @param id 102 | * @param variables 103 | * @param localVariables 104 | * @returns {Promise} 105 | */ 106 | complete({ id, variables, localVariables }) { 107 | return this.post(`/external-task/${id}/complete`, { 108 | json: { workerId: this.workerId, variables, localVariables }, 109 | }); 110 | } 111 | 112 | /** 113 | * @throws HTTPError 114 | * @param id 115 | * @param options 116 | * @returns {Promise} 117 | */ 118 | handleFailure({ id }, options) { 119 | return this.post(`/external-task/${id}/failure`, { 120 | json: { ...options, workerId: this.workerId }, 121 | }); 122 | } 123 | 124 | /** 125 | * @throws HTTPError 126 | * @param id 127 | * @param errorCode 128 | * @param errorMessage 129 | * @param variables 130 | * @returns {Promise} 131 | */ 132 | handleBpmnError({ id }, errorCode, errorMessage, variables) { 133 | return this.post(`/external-task/${id}/bpmnError`, { 134 | json: { errorCode, workerId: this.workerId, errorMessage, variables }, 135 | }); 136 | } 137 | 138 | /** 139 | * @throws HTTPError 140 | * @param id 141 | * @param lockDuration 142 | * @returns {Promise} 143 | */ 144 | lock({ id }, lockDuration) { 145 | return this.post(`/external-task/${id}/lock`, { 146 | json: { 147 | lockDuration, 148 | workerId: this.workerId, 149 | }, 150 | }); 151 | } 152 | 153 | /** 154 | * @throws HTTPError 155 | * @param id 156 | * @param newDuration 157 | * @returns {Promise} 158 | */ 159 | extendLock({ id }, newDuration) { 160 | return this.post(`/external-task/${id}/extendLock`, { 161 | json: { newDuration, workerId: this.workerId }, 162 | }); 163 | } 164 | 165 | /** 166 | * @throws HTTPError 167 | * @param id 168 | * @returns {Promise} 169 | */ 170 | unlock({ id }) { 171 | return this.post(`/external-task/${id}/unlock`, {}); 172 | } 173 | } 174 | 175 | export default EngineService; 176 | -------------------------------------------------------------------------------- /lib/TaskService.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. Camunda licenses this file to you under the Apache License, 6 | * Version 2.0; you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import { 19 | MISSING_TASK, 20 | MISSING_ERROR_CODE, 21 | MISSING_DURATION, 22 | MISSING_NEW_DURATION, 23 | } from "./__internal/errors.js"; 24 | import { isUndefinedOrNull } from "./__internal/utils.js"; 25 | import Variables from "./Variables.js"; 26 | 27 | class TaskService { 28 | constructor(events, api) { 29 | this.events = events; 30 | this.api = api; 31 | 32 | this.sanitizeTask = this.sanitizeTask.bind(this); 33 | this.success = this.success.bind(this); 34 | this.error = this.error.bind(this); 35 | this.complete = this.complete.bind(this); 36 | this.handleFailure = this.handleFailure.bind(this); 37 | this.handleBpmnError = this.handleBpmnError.bind(this); 38 | this.lock = this.lock.bind(this); 39 | this.extendLock = this.extendLock.bind(this); 40 | } 41 | 42 | sanitizeTask(task) { 43 | if (typeof task === "object") { 44 | return { id: task.id }; 45 | } else { 46 | return { id: task }; 47 | } 48 | } 49 | 50 | success(event, ...args) { 51 | this.events.emit(`${event}:success`, ...args); 52 | } 53 | 54 | error(event, ...args) { 55 | this.events.emit(`${event}:error`, ...args); 56 | } 57 | 58 | /** 59 | * @throws Error 60 | * @param task 61 | * @returns {Promise} 62 | */ 63 | async complete(task, variables, localVariables) { 64 | if (isUndefinedOrNull(task)) { 65 | throw new Error(MISSING_TASK); 66 | } 67 | 68 | const sanitizedTask = this.sanitizeTask(task); 69 | try { 70 | const requestBody = { ...sanitizedTask }; 71 | if (variables instanceof Variables) { 72 | requestBody.variables = variables.getDirtyVariables(); 73 | } 74 | if (localVariables instanceof Variables) { 75 | requestBody.localVariables = localVariables.getDirtyVariables(); 76 | } 77 | await this.api.complete(requestBody); 78 | this.success("complete", task); 79 | } catch (e) { 80 | this.error("complete", task, e); 81 | throw e; 82 | } 83 | } 84 | 85 | /** 86 | * @throws Error 87 | * @param task 88 | * @param options 89 | * @returns {Promise} 90 | */ 91 | async handleFailure(task, options) { 92 | if (isUndefinedOrNull(task)) { 93 | throw new Error(MISSING_TASK); 94 | } 95 | 96 | const sanitizedTask = this.sanitizeTask(task); 97 | try { 98 | await this.api.handleFailure(sanitizedTask, options); 99 | this.success("handleFailure", task); 100 | } catch (e) { 101 | this.error("handleFailure", task, e); 102 | throw e; 103 | } 104 | } 105 | 106 | /** 107 | * @throws Error 108 | * @param task 109 | * @param errorCode 110 | * @param errorMessage 111 | * @param variables 112 | * @returns {Promise} 113 | */ 114 | async handleBpmnError(task, errorCode, errorMessage, variables) { 115 | if (isUndefinedOrNull(task)) { 116 | throw new Error(MISSING_TASK); 117 | } 118 | if (!errorCode) { 119 | throw new Error(MISSING_ERROR_CODE); 120 | } 121 | 122 | const sanitizedTask = this.sanitizeTask(task); 123 | 124 | try { 125 | await this.api.handleBpmnError( 126 | sanitizedTask, 127 | errorCode, 128 | errorMessage, 129 | variables instanceof Variables 130 | ? variables.getDirtyVariables() 131 | : undefined 132 | ); 133 | this.success("handleBpmnError", task); 134 | } catch (e) { 135 | this.error("handleBpmnError", task, e); 136 | throw e; 137 | } 138 | } 139 | 140 | /** 141 | * @throws Error 142 | * @param task 143 | * @param duration 144 | * @returns {Promise} 145 | */ 146 | async lock(task, duration) { 147 | if (isUndefinedOrNull(task)) { 148 | throw new Error(MISSING_TASK); 149 | } 150 | if (!duration) { 151 | throw new Error(MISSING_DURATION); 152 | } 153 | 154 | const sanitizedTask = this.sanitizeTask(task); 155 | try { 156 | await this.api.lock(sanitizedTask, duration); 157 | this.success("lock", task); 158 | } catch (e) { 159 | this.error("lock", task, e); 160 | throw e; 161 | } 162 | } 163 | 164 | /** 165 | * @throws Error 166 | * @param task 167 | * @param newDuration 168 | * @returns {Promise} 169 | */ 170 | async extendLock(task, newDuration) { 171 | if (isUndefinedOrNull(task)) { 172 | throw new Error(MISSING_TASK); 173 | } 174 | if (!newDuration) { 175 | throw new Error(MISSING_NEW_DURATION); 176 | } 177 | 178 | const sanitizedTask = this.sanitizeTask(task); 179 | try { 180 | await this.api.extendLock(sanitizedTask, newDuration); 181 | this.success("extendLock", task); 182 | } catch (e) { 183 | this.error("extendLock", task, e); 184 | throw e; 185 | } 186 | } 187 | 188 | /** 189 | * @throws Error 190 | * @param task 191 | * @returns {Promise} 192 | */ 193 | async unlock(task) { 194 | if (isUndefinedOrNull(task)) { 195 | throw new Error(MISSING_TASK); 196 | } 197 | 198 | const sanitizedTask = this.sanitizeTask(task); 199 | try { 200 | await this.api.unlock(sanitizedTask); 201 | this.success("unlock", task); 202 | } catch (e) { 203 | this.error("unlock", task, e); 204 | throw e; 205 | } 206 | } 207 | } 208 | 209 | export default TaskService; 210 | -------------------------------------------------------------------------------- /lib/KeycloakAuthInterceptor.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. Camunda licenses this file to you under the Apache License, 6 | * Version 2.0; you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import got from "got"; 19 | import { 20 | MISSING_KEYCLOAK_AUTH_PARAMS, 21 | UNEXPECTED_KEYCLOAK_TOKEN_RESULT, 22 | } from "./__internal/errors.js"; 23 | 24 | /** 25 | * A KeycloakAuthInterceptor instance is an interceptor that adds a Bearer token header, containing 26 | * a access token, to all requests. This interceptor can be used if the Camunda REST API is protected with 27 | * Keycloak Gatekeeper. 28 | */ 29 | class KeycloakAuthInterceptor { 30 | /** 31 | * The class constructor. 32 | * 33 | * @param options The Keycloak auth interceptor options. 34 | * @returns function(*): {hooks: {beforeRequest: [function(*): Promise]}} The interceptor function. 35 | * @throws Error if the required options are not set. 36 | */ 37 | constructor(options) { 38 | if ( 39 | !options || 40 | !options.tokenEndpoint || 41 | !options.clientId || 42 | !options.clientSecret 43 | ) { 44 | throw new Error(MISSING_KEYCLOAK_AUTH_PARAMS); 45 | } 46 | 47 | /** 48 | * Bind member methods 49 | */ 50 | this.getAccessToken = this.getAccessToken.bind(this); 51 | this.cacheToken = this.cacheToken.bind(this); 52 | this.interceptor = this.interceptor.bind(this); 53 | 54 | this.cacheOffset = 55 | options.cacheOffset !== undefined ? options.cacheOffset : 10; 56 | this.tokenEndpoint = options.tokenEndpoint; 57 | this.clientId = options.clientId; 58 | this.clientSecret = options.clientSecret; 59 | 60 | return this.interceptor; 61 | } 62 | 63 | /** 64 | * Requests a new access token from the Keycloak endpoint. 65 | * 66 | * @param tokenEndpoint The URL to the Keycloak token endpoint. 67 | * @param clientID The Keycloak client ID. 68 | * @param clientSecret The Keycloak client secret. 69 | * @returns {Promise<{access_token: String, expires_in: Number}>} The token response, containing the access token and 70 | * it's expiry in seconds. 71 | * @throws Error if an error occurred during the request. 72 | */ 73 | async getAccessToken(tokenEndpoint, clientID, clientSecret) { 74 | const credentials = Buffer.from(`${clientID}:${clientSecret}`).toString( 75 | "base64" 76 | ); 77 | 78 | try { 79 | return await got(tokenEndpoint, { 80 | method: "POST", 81 | headers: { 82 | "Content-Type": "application/x-www-form-urlencoded", 83 | Authorization: `Basic: ${credentials}`, 84 | }, 85 | body: "grant_type=client_credentials", 86 | }).json(); 87 | } catch (e) { 88 | throw new Error( 89 | `${UNEXPECTED_KEYCLOAK_TOKEN_RESULT} status: ${e.response.statusCode}; body: ${e.response.body}` 90 | ); 91 | } 92 | } 93 | 94 | /** 95 | * The Keycloak access token has an expiry defined. To reduce the requests to the token endpoint, we 96 | * cache the token response as long it is valid. 97 | * 98 | * To poll the API always with a valid token, we subtract the `cacheOffset` from the validity. If the token is 60 99 | * seconds valid and we poll the API every 5 seconds, there could be the case that we poll the API exactly at the 100 | * time the token expires. The default `cacheOffset` from 10 seconds is a good balance between the token validity 101 | * and the average request duration. 102 | * 103 | * If the `expires_in` property wasn't set or if the validity is less than or equal the `cacheOffset`, we don't 104 | * cache the token. 105 | * 106 | * @param {{access_token: String, expires_in: Number}} token The token response. 107 | */ 108 | cacheToken(token) { 109 | const expiresIn = token.expires_in || 0; 110 | 111 | if (expiresIn > this.cacheOffset) { 112 | this.tokenCache = token; 113 | const tokenCleaner = () => { 114 | this.tokenCache = null; 115 | }; 116 | setTimeout( 117 | tokenCleaner.bind(this), 118 | (expiresIn - this.cacheOffset) * 1000 119 | ); 120 | } 121 | } 122 | 123 | /** 124 | * The interceptor function that enriches the `got` request with the access token hook. 125 | * 126 | * @param config The `got` request config. 127 | * @returns {{hooks: {beforeRequest: [function(*): Promise]}}} The `got` config containing the access 128 | * token hook. 129 | */ 130 | interceptor(config) { 131 | // https://www.npmjs.com/package/got#hooksbeforerequest 132 | const hooks = { 133 | beforeRequest: [ 134 | async (options) => { 135 | let token = this.tokenCache; 136 | if (!token) { 137 | token = await this.getAccessToken( 138 | this.tokenEndpoint, 139 | this.clientId, 140 | this.clientSecret 141 | ); 142 | this.cacheToken(token); 143 | } 144 | 145 | if (token && token.access_token) { 146 | const defaultHeaders = options.headers || {}; 147 | const headers = { 148 | Authorization: `Bearer ${token.access_token}`, 149 | }; 150 | 151 | options.headers = { ...defaultHeaders, ...headers }; 152 | } else { 153 | throw new Error( 154 | `${UNEXPECTED_KEYCLOAK_TOKEN_RESULT} token without access_token property: ${JSON.stringify( 155 | token 156 | )}` 157 | ); 158 | } 159 | }, 160 | ], 161 | }; 162 | 163 | return { ...config, hooks: { ...config.hooks, ...hooks } }; 164 | } 165 | } 166 | 167 | export default KeycloakAuthInterceptor; 168 | -------------------------------------------------------------------------------- /lib/Variables.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. Camunda licenses this file to you under the Apache License, 6 | * Version 2.0; you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import { Variables, File } from "../index.js"; 19 | 20 | describe("Variables", () => { 21 | describe("read-only", () => { 22 | it("should only have getters if readOnly is true", () => { 23 | // given 24 | const readOnlyVariables = new Variables({}, { readOnly: true }); 25 | 26 | // then 27 | expect(Object.keys(readOnlyVariables)).toMatchSnapshot(); 28 | }); 29 | 30 | it("should have getters and setters if readOnly is not true", () => { 31 | // given 32 | const readOnlyVariables = new Variables({}); 33 | 34 | // then 35 | expect(Object.keys(readOnlyVariables)).toMatchSnapshot(); 36 | }); 37 | }); 38 | 39 | describe("getters", () => { 40 | let variables; 41 | beforeEach(() => { 42 | variables = new Variables({ 43 | foo: { type: "string", value: "FooValue", valueInfo: {} }, 44 | bar: { type: "integer", value: 2, valueInfo: {} }, 45 | baz: { type: "json", value: '{"name":"baz"}', valueInfo: {} }, 46 | qux: { 47 | type: "date", 48 | value: new Date("2018-01-23T14:42:45.435+0200"), 49 | valueInfo: {}, 50 | }, 51 | zex: { 52 | type: "file", 53 | value: null, 54 | valueInfo: {}, 55 | }, 56 | }); 57 | 58 | const file = new File({ localPath: "some/local/path" }); 59 | file.content = Buffer.from("some content"); 60 | 61 | variables.setTyped("blax", { 62 | type: "file", 63 | value: file, 64 | valueInfo: {}, 65 | }); 66 | }); 67 | 68 | it("getAllTyped() should return all variables", () => { 69 | expect(variables.getAllTyped()).toMatchSnapshot(); 70 | }); 71 | 72 | it("getAll() should return values of all variables", () => { 73 | expect(variables.getAll()).toMatchSnapshot(); 74 | }); 75 | 76 | it("getDirtyVariables() should return all dirty variables", () => { 77 | expect(variables.getDirtyVariables()).toMatchSnapshot(); 78 | }); 79 | 80 | it("get('foo') should return value of key foo", () => { 81 | expect(variables.get("foo")).toMatchSnapshot(); 82 | }); 83 | 84 | it("getTyped('non_existing_key') should return null", () => { 85 | expect(variables.getTyped("non_existing_key")).toBeNull(); 86 | }); 87 | 88 | it("getTyped('foo') should return the typed value of key foo", () => { 89 | expect(variables.getTyped("foo")).toMatchSnapshot(); 90 | }); 91 | }); 92 | 93 | describe("setters", () => { 94 | let variables; 95 | beforeEach(() => { 96 | variables = new Variables(); 97 | }); 98 | 99 | it('setTyped("baz",someTypeValue) should set typed value with key "baz"', () => { 100 | // given 101 | expect(variables.getAllTyped()).toMatchSnapshot("variables"); 102 | expect(variables.getDirtyVariables()).toMatchSnapshot("dirty variables"); 103 | const key = "baz"; 104 | const typedValue = { 105 | type: "json", 106 | value: { name: "bazname" }, 107 | valueInfo: {}, 108 | }; 109 | 110 | // when 111 | variables.setTyped(key, typedValue); 112 | 113 | // then 114 | expect(variables.getAllTyped()).toMatchSnapshot("variables"); 115 | expect(variables.getDirtyVariables()).toMatchSnapshot("dirty variables"); 116 | }); 117 | 118 | it("setAllTyped(someTypedValues) should add someTypedValues to variables", () => { 119 | // given 120 | expect(variables.getAllTyped()).toMatchSnapshot("variables"); 121 | expect(variables.getDirtyVariables()).toMatchSnapshot("dirty variables"); 122 | const typedValues = { 123 | foo: { value: "fooValue", type: "string", valueInfo: {} }, 124 | }; 125 | variables.set("bar", "barValue"); 126 | 127 | // when 128 | variables.setAllTyped(typedValues); 129 | 130 | // then 131 | expect(variables.getAllTyped()).toMatchSnapshot("variables"); 132 | expect(variables.getDirtyVariables()).toMatchSnapshot("dirty variables"); 133 | }); 134 | 135 | it('set("foo", "fooValue")) should set variable with key "foo" and value "fooValue"', () => { 136 | // given 137 | expect(variables.getAllTyped()).toMatchSnapshot("variables"); 138 | expect(variables.getDirtyVariables()).toMatchSnapshot("dirty variables"); 139 | variables.set("foo", "fooValue"); 140 | 141 | // then 142 | expect(variables.getAllTyped()).toMatchSnapshot("variables"); 143 | expect(variables.getDirtyVariables()).toMatchSnapshot("dirty variables"); 144 | }); 145 | 146 | it('setTransient("fooTransient", "fooValueTransient")) should set variable with key "fooTransient" and value "fooValueTransient" and transient "true"', () => { 147 | // given 148 | expect(variables.getAllTyped()).toMatchSnapshot("variables"); 149 | expect(variables.getDirtyVariables()).toMatchSnapshot("dirty variables"); 150 | variables.setTransient("fooTransient", "fooValueTransient"); 151 | 152 | // then 153 | expect(variables.getAllTyped()).toMatchSnapshot("variables"); 154 | expect(variables.getDirtyVariables()).toMatchSnapshot("dirty variables"); 155 | }); 156 | 157 | it("setAll(someValues) should add someValues to variables", () => { 158 | // given 159 | expect(variables.getAllTyped()).toMatchSnapshot("variables"); 160 | expect(variables.getDirtyVariables()).toMatchSnapshot("dirty variables"); 161 | const someValues = { 162 | foo: "FooValue", 163 | bar: 2, 164 | }; 165 | 166 | // when 167 | variables.setAll(someValues); 168 | 169 | // then 170 | // given 171 | expect(variables.getAllTyped()).toMatchSnapshot("variables"); 172 | expect(variables.getDirtyVariables()).toMatchSnapshot("dirty variables"); 173 | }); 174 | }); 175 | }); 176 | -------------------------------------------------------------------------------- /docs/Variables.md: -------------------------------------------------------------------------------- 1 | # Variables 2 | ```js 3 | const { Variables } = require("camunda-external-task-client-js"); 4 | 5 | // ... somewhere in the handler function 6 | const variables = new Variables().setAll({ foo: "some foo value" }); 7 | console.log("foo", variables.get("foo")); 8 | ``` 9 | 10 | > **Note:** All setters return the variables instance. This can be helpful 11 | for chaining calls. 12 | 13 | 14 | ## `new Variables(options)` 15 | 16 | The following option can be passed but is **not required**: 17 | 18 | | Option | Description | Type | 19 | |----------|-------------------------------------------------------|---------| 20 | | readOnly | If set to true, only getters functions are available. | boolean | 21 | 22 | ## `variables.get(variableName)` 23 | 24 | Returns the value of the variable with key _variableName_. 25 | 26 | ```js 27 | // Given: 28 | // score: { value: 5, type: "integer", valueInfo: {} } 29 | const score = variables.get("score"); 30 | console.log(score); 31 | ``` 32 | 33 | Output: 34 | 35 | ```js 36 | 5 37 | ``` 38 | 39 | ## `variables.getTyped(variableName)` 40 | 41 | Returns the typed value of the variable with key _variableName_. 42 | 43 | ```js 44 | // Given 45 | // score: { value: 5, type: "integer", valueInfo: {} } 46 | const score = variables.getTyped("score"); 47 | console.log(score); 48 | ``` 49 | 50 | Output: 51 | 52 | ```js 53 | { value: 5, type: "integer", valueInfo: {} } 54 | ``` 55 | 56 | ### About _typed values_ 57 | A typed value is an object with the following structure: 58 | 59 | ``` 60 | { 61 | value: "some value", 62 | // type of the variable, e.g. integer, long, string, boolean, json ... 63 | type: "string", 64 | // An object containing additional, value-type-dependent properties 65 | valueInfo: {} 66 | } 67 | ``` 68 | 69 | ## `variables.getAll()` 70 | 71 | Returns the values of all variables. 72 | 73 | ```js 74 | // Given: 75 | // { 76 | // score: { value: 5, type: "integer", valueInfo: {} } 77 | // isWinning: { value: false, type: "boolean", valueInfo: {} } 78 | // } 79 | const values = variables.getAll(); 80 | console.log(values); 81 | ``` 82 | 83 | Output: 84 | 85 | ```js 86 | { score: 5, isWinning: false } 87 | ``` 88 | 89 | ## `variables.getAllTyped()` 90 | 91 | Returns the typed values of all variables. 92 | 93 | ```js 94 | // Given: 95 | // { 96 | // score: { value: 5, type: "integer", valueInfo: {} } 97 | // isWinning: { value: false, type: "boolean", valueInfo: {} } 98 | // } 99 | const typedValues = variables.getAllTyped(); 100 | console.log(typedValues); 101 | ``` 102 | 103 | Output: 104 | 105 | ```js 106 | { 107 | score: { value: 5, type: "integer", valueInfo: {} }, 108 | isWinning: { value: false, type: "boolean", valueInfo: {} } 109 | } 110 | ``` 111 | 112 | ## `variables.set(variableName, value)` 113 | 114 | Sets a value for the variable with key _variableName_. 115 | 116 | > **Note:** The variable type is determined automatically. 117 | 118 | ```js 119 | variables.set("fullName", { first: "John", last: "Doe" }); 120 | console.log(variables.getTyped("fullName")); 121 | ``` 122 | 123 | Output: 124 | 125 | ```js 126 | { 127 | value: { first: "John", last: "Doe" }, 128 | type: "json", 129 | valueInfo: {} 130 | } 131 | ``` 132 | 133 | ## `variables.setTransient(variableName, value)` 134 | 135 | Sets a value for the variable with key _variableName_, also sets transient flag true to variable. 136 | 137 | > **Note:** The variable type is determined automatically. 138 | 139 | ```js 140 | variables.setTransient("fullName", { first: "John", last: "Doe" }); 141 | console.log(variables.getTyped("fullName")); 142 | ``` 143 | 144 | Output: 145 | 146 | ```js 147 | { 148 | value: { first: "John", last: "Doe" }, 149 | type: "json", 150 | valueInfo: {transient: true} 151 | } 152 | ``` 153 | 154 | ## `variables.setTyped(variableName, typedValue)` 155 | 156 | Sets a typed value for the variable with key _variableName_ 157 | 158 | >**Note:** The variable type is **not** case sensitive. 159 | 160 | ```js 161 | variables.setTyped("test", { 162 | value: "", 163 | type: "XML", 164 | valueInfo: {} 165 | }); 166 | 167 | console.log(variables.getTyped("test")); 168 | ``` 169 | 170 | Output 171 | 172 | ```js 173 | { 174 | value: "", 175 | type: "XML", 176 | valueInfo: {} 177 | } 178 | ``` 179 | 180 | ## `variables.setAll(values)` 181 | 182 | Sets the values of multiple variables at once. 183 | 184 | ```js 185 | // Given: 186 | // { 187 | // score: { value: 6, type: "integer", valueInfo: {} } 188 | // isWinning: { value: true, type: "boolean", valueInfo: {} } 189 | // } 190 | variables.setAll({ 191 | score: 8, 192 | message: "Score is on 🔥" 193 | }); 194 | 195 | console.log(variables.getAll()); 196 | ``` 197 | 198 | Output: 199 | 200 | ```js 201 | { score: 8, isWinning: true, message: "Score is on 🔥" } 202 | ``` 203 | 204 | ## `variables.setAllTyped(typedValues)` 205 | 206 | Sets the typed values of multiple variables at once. 207 | 208 | ```js 209 | // Given: 210 | // { 211 | // score: { value: 6, type: "integer", valueInfo: {} } 212 | // isWinning: { value: true, type: "boolean", valueInfo: {} } 213 | // } 214 | variables.setAllTyped({ 215 | score: { value: 8, type: "short", valueInfo: {} }, 216 | message: { value: "Score is on 🔥", type: "string", valueInfo: {} } 217 | }); 218 | 219 | console.log(variables.getAllTyped()); 220 | ``` 221 | 222 | Output: 223 | 224 | ```js 225 | { 226 | score: { value: 8 , type: "short", valueInfo: {} }, 227 | isWinning: { value: true, type: "boolean", valueInfo: {} }, 228 | message: { value: "Score is on 🔥" , type: "string", valueInfo: {} }, 229 | } 230 | ``` 231 | 232 | ## About JSON & Date Variables 233 | Date and JSON values are automatically serialized when being set and deserialized when being read. 234 | 235 | ### Date 236 | 237 | ```js 238 | // 'variables.set()' can be used to set a date by providing a date object 239 | variables.set("someDate", new Date()); 240 | 241 | // 'variables.setTyped()' can be used to set a date by either: 242 | // 1- providing a date object as a value: 243 | variables.setTyped("anotherDate", { type: "date", value: new Date(), valueInfo: {} }); 244 | // 2- providing a date string as a value: 245 | variables.setTyped("anotherDate", { type: "date", value: "2016-01-25T13:33:42.165+0100", valueInfo: {} }); 246 | 247 | // `variables.get("anotherDate")` is a date object 248 | console.log(typeof variables.get("anotherDate")); // output: object 249 | 250 | // `variables.getTyped("anotherDate").value` is date object 251 | console.log(typeof variables.getTyped("anotherDate").value); // output: object 252 | ``` 253 | 254 | ### JSON 255 | ```js 256 | // 'variables.set()' can be used to set a JSON object by providing an object 257 | variables.set("meal", { id: 0, name: "pasta" }); 258 | 259 | // The same is also possible with `variables.setTyped()` 260 | variables.setTyped({ 261 | type: "json", 262 | value: { id: 0, name: "pasta" }, 263 | valueInfo: {} 264 | }); 265 | 266 | // `variables.get("meal")` is an object 267 | console.log(variables.get("someJSON")); // output: { id: 0, name: "pasta" } 268 | 269 | // `variables.getTyped("meal").value` is an object 270 | console.log(variables.getTyped("someJSON").value); // output: { id: 0, name: "pasta" } 271 | ``` 272 | -------------------------------------------------------------------------------- /lib/logger.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. Camunda licenses this file to you under the Apache License, 6 | * Version 2.0; you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import { jest } from "@jest/globals"; 19 | import logger from "./logger.js"; 20 | import events from "events"; 21 | 22 | describe("logger", () => { 23 | let client; 24 | 25 | describe("events", () => { 26 | beforeAll(() => { 27 | client = new events(); 28 | logger(client, "silly"); 29 | }); 30 | 31 | beforeEach(() => { 32 | jest.spyOn(console, "log").mockImplementation(() => jest.fn()); 33 | }); 34 | 35 | afterEach(() => { 36 | console.log.mockClear(); 37 | }); 38 | 39 | it("should log subscribe event", () => { 40 | // when 41 | client.emit("subscribe", "some topic"); 42 | 43 | // then 44 | expect(console.log.mock.calls[0][0]).toMatchSnapshot(); 45 | }); 46 | 47 | it("should log unsubscribe event", () => { 48 | // when 49 | client.emit("unsubscribe", "some topic"); 50 | 51 | // then 52 | expect(console.log.mock.calls[0][0]).toMatchSnapshot(); 53 | }); 54 | 55 | it("should log poll:start event", () => { 56 | // when 57 | client.emit("poll:start"); 58 | 59 | // then 60 | expect(console.log.mock.calls[0][0]).toMatchSnapshot(); 61 | }); 62 | 63 | it("should log poll:stop event", () => { 64 | // when 65 | client.emit("poll:stop"); 66 | 67 | // then 68 | expect(console.log.mock.calls[0][0]).toMatchSnapshot(); 69 | }); 70 | 71 | it("should log poll:success event", () => { 72 | // when 73 | client.emit("poll:success", ["task1", "task2"]); 74 | 75 | // then 76 | expect(console.log.mock.calls[0][0]).toMatchSnapshot(); 77 | }); 78 | 79 | it("should log poll:error event", () => { 80 | // when 81 | client.emit("poll:error"); 82 | 83 | // then 84 | expect(console.log.mock.calls[0][0]).toMatchSnapshot(); 85 | }); 86 | 87 | it("should log complete:success", () => { 88 | // when 89 | client.emit("complete:success", { id: "some task id" }); 90 | 91 | // then 92 | expect(console.log.mock.calls[0][0]).toMatchSnapshot(); 93 | }); 94 | 95 | it("should log complete:error", () => { 96 | // when 97 | client.emit("complete:error", { id: "some task id" }, "some error"); 98 | 99 | // then 100 | expect(console.log.mock.calls[0][0]).toMatchSnapshot(); 101 | }); 102 | 103 | it("should log handleFailure:success", () => { 104 | // when 105 | client.emit("handleFailure:success", { id: "some task id" }); 106 | 107 | // then 108 | expect(console.log.mock.calls[0][0]).toMatchSnapshot(); 109 | }); 110 | 111 | it("should log handleFailure:error", () => { 112 | // when 113 | client.emit("handleFailure:error", { id: "some task id" }, "some error"); 114 | 115 | // then 116 | expect(console.log.mock.calls[0][0]).toMatchSnapshot(); 117 | }); 118 | 119 | it("should log handleBpmnError:success", () => { 120 | // when 121 | client.emit("handleBpmnError:success", { id: "some task id" }); 122 | 123 | // then 124 | expect(console.log.mock.calls[0][0]).toMatchSnapshot(); 125 | }); 126 | 127 | it("should log handleBpmnError:error", () => { 128 | // when 129 | client.emit( 130 | "handleBpmnError:error", 131 | { id: "some task id" }, 132 | "some error" 133 | ); 134 | 135 | // then 136 | expect(console.log.mock.calls[0][0]).toMatchSnapshot(); 137 | }); 138 | 139 | it("should log extendLock:success", () => { 140 | // when 141 | client.emit("extendLock:success", { id: "some task id" }); 142 | 143 | // then 144 | expect(console.log.mock.calls[0][0]).toMatchSnapshot(); 145 | }); 146 | 147 | it("should log extendLock:error", () => { 148 | // when 149 | client.emit("extendLock:error", { id: "some task id" }, "some error"); 150 | 151 | // then 152 | expect(console.log.mock.calls[0][0]).toMatchSnapshot(); 153 | }); 154 | 155 | it("should log unlock:success", () => { 156 | // when 157 | client.emit("unlock:success", { id: "some task id" }); 158 | 159 | // then 160 | expect(console.log.mock.calls[0][0]).toMatchSnapshot(); 161 | }); 162 | 163 | it("should log unlock:error", () => { 164 | // when 165 | client.emit("unlock:error", { id: "some task id" }, "some error"); 166 | 167 | // then 168 | expect(console.log.mock.calls[0][0]).toMatchSnapshot(); 169 | }); 170 | }); 171 | 172 | describe("config", () => { 173 | beforeEach(() => { 174 | client = new events(); 175 | jest.spyOn(console, "log").mockImplementation(() => jest.fn()); 176 | }); 177 | 178 | afterEach(() => { 179 | console.log.mockClear(); 180 | }); 181 | 182 | it("should have debug level", () => { 183 | // when 184 | logger(client, "debug"); 185 | 186 | client.emit("poll:start"); // debug 187 | client.emit("subscribe", "some topic"); // info 188 | client.emit("poll:error"); // error 189 | 190 | // then 191 | expect(console.log).toHaveBeenCalledTimes(3); 192 | }); 193 | 194 | it("should have info level", () => { 195 | // when 196 | logger(client); 197 | 198 | client.emit("poll:start"); // debug 199 | client.emit("subscribe", "some topic"); // info 200 | client.emit("poll:error"); // error 201 | 202 | // then 203 | expect(console.log).toHaveBeenCalledTimes(2); 204 | }); 205 | 206 | it("should have error level", () => { 207 | // when 208 | logger(client, "error"); 209 | 210 | client.emit("poll:start"); // debug 211 | client.emit("subscribe", "some topic"); // info 212 | client.emit("poll:error"); // error 213 | 214 | // then 215 | expect(console.log).toHaveBeenCalledTimes(1); 216 | }); 217 | 218 | it("should use info level on default", () => { 219 | // when 220 | logger(client); 221 | 222 | client.emit("poll:start"); // debug 223 | client.emit("subscribe", "some topic"); // info 224 | client.emit("poll:error"); // error 225 | 226 | // then 227 | expect(console.log).toHaveBeenCalledTimes(2); 228 | }); 229 | 230 | it("should allow NPM log level numbers", () => { 231 | // when 232 | logger(client, 2); // info 233 | 234 | client.emit("poll:start"); // debug 235 | client.emit("subscribe", "some topic"); // info 236 | client.emit("poll:error"); // error 237 | 238 | // then 239 | expect(console.log).toHaveBeenCalledTimes(2); 240 | }); 241 | }); 242 | }); 243 | -------------------------------------------------------------------------------- /lib/KeycloakAuthInterceptor.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. Camunda licenses this file to you under the Apache License, 6 | * Version 2.0; you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import { jest } from "@jest/globals"; 19 | import got from "got"; 20 | import KeycloakAuthInterceptor from "./KeycloakAuthInterceptor.js"; 21 | import { 22 | MISSING_KEYCLOAK_AUTH_PARAMS, 23 | UNEXPECTED_KEYCLOAK_TOKEN_RESULT, 24 | } from "./__internal/errors.js"; 25 | 26 | describe("KeycloakAuthInterceptor", () => { 27 | afterEach(() => { 28 | got.mockClear(); 29 | }); 30 | 31 | test("should throw error if tokenEndpoint, clientId or clientSecret are missing", () => { 32 | expect(() => new KeycloakAuthInterceptor()).toThrowError( 33 | MISSING_KEYCLOAK_AUTH_PARAMS 34 | ); 35 | expect( 36 | () => new KeycloakAuthInterceptor({ tokenEndpoint: "some endpoint" }) 37 | ).toThrowError(MISSING_KEYCLOAK_AUTH_PARAMS); 38 | expect( 39 | () => new KeycloakAuthInterceptor({ clientId: "some id" }) 40 | ).toThrowError(MISSING_KEYCLOAK_AUTH_PARAMS); 41 | expect( 42 | () => new KeycloakAuthInterceptor({ clientSecret: "some secret" }) 43 | ).toThrowError(MISSING_KEYCLOAK_AUTH_PARAMS); 44 | expect( 45 | () => 46 | new KeycloakAuthInterceptor({ 47 | tokenEndpoint: "some endpoint", 48 | clientId: "some id", 49 | }) 50 | ).toThrowError(MISSING_KEYCLOAK_AUTH_PARAMS); 51 | expect( 52 | () => 53 | new KeycloakAuthInterceptor({ 54 | tokenEndpoint: "some endpoint", 55 | clientSecret: "some secret", 56 | }) 57 | ).toThrowError(MISSING_KEYCLOAK_AUTH_PARAMS); 58 | expect( 59 | () => 60 | new KeycloakAuthInterceptor({ 61 | clientId: "some password", 62 | clientSecret: "some secret", 63 | }) 64 | ).toThrowError(MISSING_KEYCLOAK_AUTH_PARAMS); 65 | }); 66 | 67 | test("should throw error if token endpoint returns unexpected HTTP response", async () => { 68 | // given 69 | const response = { 70 | body: "Some keycloak error", 71 | statusCode: 400, 72 | statusMessage: "Bad request", 73 | }; 74 | const error = new got.HTTPError(response); 75 | error.response = response; 76 | got.mockReturnValue({ json: () => Promise.reject(error) }); 77 | const keycloakAuthInterceptor = new KeycloakAuthInterceptor({ 78 | tokenEndpoint: "some endpoint", 79 | clientId: "some id", 80 | clientSecret: "some secret", 81 | }); 82 | 83 | // when 84 | const { hooks } = keycloakAuthInterceptor({}); 85 | const hook = hooks.beforeRequest[0]; 86 | 87 | // then 88 | try { 89 | await hook({}); 90 | } catch (e) { 91 | expect(e).toEqual( 92 | new Error( 93 | `${UNEXPECTED_KEYCLOAK_TOKEN_RESULT} status: 400; body: Some keycloak error` 94 | ) 95 | ); 96 | } 97 | }); 98 | 99 | test("should throw error if token doesn't contains the access token", async () => { 100 | // given 101 | got.mockReturnValue({ json: () => Promise.resolve({}) }); 102 | const keycloakAuthInterceptor = new KeycloakAuthInterceptor({ 103 | tokenEndpoint: "some endpoint", 104 | clientId: "some id", 105 | clientSecret: "some secret", 106 | }); 107 | 108 | // when 109 | const { hooks } = keycloakAuthInterceptor({}); 110 | const hook = hooks.beforeRequest[0]; 111 | 112 | // then 113 | try { 114 | await hook({}); 115 | } catch (e) { 116 | expect(e).toEqual( 117 | new Error( 118 | `${UNEXPECTED_KEYCLOAK_TOKEN_RESULT} token without access_token property: {}` 119 | ) 120 | ); 121 | } 122 | }); 123 | 124 | test("should add auth token to intercepted config", async () => { 125 | // given 126 | got.mockReturnValue({ 127 | json: () => Promise.resolve({ access_token: "1234567890" }), 128 | }); 129 | const keycloakAuthInterceptor = new KeycloakAuthInterceptor({ 130 | tokenEndpoint: "some endpoint", 131 | clientId: "some id", 132 | clientSecret: "some secret", 133 | }); 134 | const options = { key: "some value" }; 135 | 136 | // when 137 | const { hooks } = keycloakAuthInterceptor({}); 138 | const hook = hooks.beforeRequest[0]; 139 | await hook(options); 140 | 141 | // then 142 | expect(options).toMatchSnapshot(); 143 | }); 144 | 145 | test("should cache the token response if token expiry is greater than cacheOffset", async () => { 146 | // given 147 | const tokenResponse = { access_token: "1234567890", expires_in: 5 }; 148 | got.mockReturnValue({ json: () => Promise.resolve(tokenResponse) }); 149 | const keycloakAuthInterceptor = new KeycloakAuthInterceptor({ 150 | tokenEndpoint: "some endpoint", 151 | clientId: "some id", 152 | clientSecret: "some secret", 153 | cacheOffset: 0, 154 | }); 155 | 156 | // when 157 | const { hooks } = keycloakAuthInterceptor({}); 158 | const hook = hooks.beforeRequest[0]; 159 | await hook({}); 160 | await hook({}); 161 | 162 | // then 163 | expect(got).toHaveBeenCalledTimes(1); 164 | }); 165 | 166 | test("should not cache the token response if token expiry is less than cacheOffset", async () => { 167 | // given 168 | const tokenResponse = { access_token: "1234567890", expires_in: 5 }; 169 | got.mockReturnValue({ json: () => Promise.resolve(tokenResponse) }); 170 | const keycloakAuthInterceptor = new KeycloakAuthInterceptor({ 171 | tokenEndpoint: "some endpoint", 172 | clientId: "some id", 173 | clientSecret: "some secret", 174 | cacheOffset: 10, 175 | }); 176 | 177 | // when 178 | const { hooks } = keycloakAuthInterceptor({}); 179 | const hook = hooks.beforeRequest[0]; 180 | await hook({}); 181 | await hook({}); 182 | 183 | // then 184 | expect(got).toHaveBeenCalledTimes(2); 185 | }); 186 | 187 | test("should clear the token cache", async () => { 188 | jest.useFakeTimers(); 189 | 190 | // given 191 | const tokenResponse = { access_token: "1234567890", expires_in: 5 }; 192 | got.mockReturnValue({ json: () => Promise.resolve(tokenResponse) }); 193 | const keycloakAuthInterceptor = new KeycloakAuthInterceptor({ 194 | tokenEndpoint: "some endpoint", 195 | clientId: "some id", 196 | clientSecret: "some secret", 197 | cacheOffset: 0, 198 | }); 199 | 200 | // when 201 | const { hooks } = keycloakAuthInterceptor({}); 202 | const hook = hooks.beforeRequest[0]; 203 | await hook({}); 204 | jest.runAllTimers(); 205 | await hook({}); 206 | 207 | // then 208 | expect(got).toHaveBeenCalledTimes(2); 209 | 210 | jest.useRealTimers(); 211 | }); 212 | }); 213 | -------------------------------------------------------------------------------- /lib/__snapshots__/Variables.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Variables getters get('foo') should return value of key foo 1`] = `"FooValue"`; 4 | 5 | exports[`Variables getters getAll() should return values of all variables 1`] = ` 6 | { 7 | "bar": 2, 8 | "baz": { 9 | "name": "baz", 10 | }, 11 | "blax": File { 12 | "__readFile": [Function], 13 | "content": "", 14 | "createTypedValue": [Function], 15 | "engineService": undefined, 16 | "filename": "path", 17 | "load": [Function], 18 | "remotePath": "/execution/undefined/localVariables/blax/data", 19 | }, 20 | "foo": "FooValue", 21 | "qux": 2018-01-23T12:42:45.435Z, 22 | "zex": File { 23 | "__readFile": [Function], 24 | "content": "", 25 | "createTypedValue": [Function], 26 | "engineService": undefined, 27 | "filename": "data", 28 | "load": [Function], 29 | "remotePath": "/execution/undefined/localVariables/zex/data", 30 | }, 31 | } 32 | `; 33 | 34 | exports[`Variables getters getAllTyped() should return all variables 1`] = ` 35 | { 36 | "bar": { 37 | "type": "integer", 38 | "value": 2, 39 | "valueInfo": {}, 40 | }, 41 | "baz": { 42 | "type": "json", 43 | "value": { 44 | "name": "baz", 45 | }, 46 | "valueInfo": {}, 47 | }, 48 | "blax": { 49 | "type": "file", 50 | "value": File { 51 | "__readFile": [Function], 52 | "content": "", 53 | "createTypedValue": [Function], 54 | "engineService": undefined, 55 | "filename": "path", 56 | "load": [Function], 57 | "remotePath": "/execution/undefined/localVariables/blax/data", 58 | }, 59 | "valueInfo": { 60 | "filename": "path", 61 | }, 62 | }, 63 | "foo": { 64 | "type": "string", 65 | "value": "FooValue", 66 | "valueInfo": {}, 67 | }, 68 | "qux": { 69 | "type": "date", 70 | "value": 2018-01-23T12:42:45.435Z, 71 | "valueInfo": {}, 72 | }, 73 | "zex": { 74 | "type": "file", 75 | "value": File { 76 | "__readFile": [Function], 77 | "content": "", 78 | "createTypedValue": [Function], 79 | "engineService": undefined, 80 | "filename": "data", 81 | "load": [Function], 82 | "remotePath": "/execution/undefined/localVariables/zex/data", 83 | }, 84 | "valueInfo": {}, 85 | }, 86 | } 87 | `; 88 | 89 | exports[`Variables getters getDirtyVariables() should return all dirty variables 1`] = ` 90 | { 91 | "blax": { 92 | "type": "file", 93 | "value": "c29tZSBjb250ZW50", 94 | "valueInfo": { 95 | "filename": "path", 96 | }, 97 | }, 98 | } 99 | `; 100 | 101 | exports[`Variables getters getTyped('foo') should return the typed value of key foo 1`] = ` 102 | { 103 | "type": "string", 104 | "value": "FooValue", 105 | "valueInfo": {}, 106 | } 107 | `; 108 | 109 | exports[`Variables read-only should have getters and setters if readOnly is not true 1`] = ` 110 | [ 111 | "getTyped", 112 | "get", 113 | "getAll", 114 | "getAllTyped", 115 | "getDirtyVariables", 116 | "setTyped", 117 | "set", 118 | "setTransient", 119 | "setAll", 120 | "setAllTyped", 121 | ] 122 | `; 123 | 124 | exports[`Variables read-only should only have getters if readOnly is true 1`] = ` 125 | [ 126 | "getTyped", 127 | "get", 128 | "getAll", 129 | "getAllTyped", 130 | "getDirtyVariables", 131 | ] 132 | `; 133 | 134 | exports[`Variables setters set("foo", "fooValue")) should set variable with key "foo" and value "fooValue": dirty variables 1`] = `{}`; 135 | 136 | exports[`Variables setters set("foo", "fooValue")) should set variable with key "foo" and value "fooValue": dirty variables 2`] = ` 137 | { 138 | "foo": { 139 | "type": "string", 140 | "value": "fooValue", 141 | "valueInfo": {}, 142 | }, 143 | } 144 | `; 145 | 146 | exports[`Variables setters set("foo", "fooValue")) should set variable with key "foo" and value "fooValue": variables 1`] = `{}`; 147 | 148 | exports[`Variables setters set("foo", "fooValue")) should set variable with key "foo" and value "fooValue": variables 2`] = ` 149 | { 150 | "foo": { 151 | "type": "string", 152 | "value": "fooValue", 153 | "valueInfo": {}, 154 | }, 155 | } 156 | `; 157 | 158 | exports[`Variables setters setAll(someValues) should add someValues to variables: dirty variables 1`] = `{}`; 159 | 160 | exports[`Variables setters setAll(someValues) should add someValues to variables: dirty variables 2`] = ` 161 | { 162 | "bar": { 163 | "type": "integer", 164 | "value": 2, 165 | "valueInfo": {}, 166 | }, 167 | "foo": { 168 | "type": "string", 169 | "value": "FooValue", 170 | "valueInfo": {}, 171 | }, 172 | } 173 | `; 174 | 175 | exports[`Variables setters setAll(someValues) should add someValues to variables: variables 1`] = `{}`; 176 | 177 | exports[`Variables setters setAll(someValues) should add someValues to variables: variables 2`] = ` 178 | { 179 | "bar": { 180 | "type": "integer", 181 | "value": 2, 182 | "valueInfo": {}, 183 | }, 184 | "foo": { 185 | "type": "string", 186 | "value": "FooValue", 187 | "valueInfo": {}, 188 | }, 189 | } 190 | `; 191 | 192 | exports[`Variables setters setAllTyped(someTypedValues) should add someTypedValues to variables: dirty variables 1`] = `{}`; 193 | 194 | exports[`Variables setters setAllTyped(someTypedValues) should add someTypedValues to variables: dirty variables 2`] = ` 195 | { 196 | "bar": { 197 | "type": "string", 198 | "value": "barValue", 199 | "valueInfo": {}, 200 | }, 201 | "foo": { 202 | "type": "string", 203 | "value": "fooValue", 204 | "valueInfo": {}, 205 | }, 206 | } 207 | `; 208 | 209 | exports[`Variables setters setAllTyped(someTypedValues) should add someTypedValues to variables: variables 1`] = `{}`; 210 | 211 | exports[`Variables setters setAllTyped(someTypedValues) should add someTypedValues to variables: variables 2`] = ` 212 | { 213 | "bar": { 214 | "type": "string", 215 | "value": "barValue", 216 | "valueInfo": {}, 217 | }, 218 | "foo": { 219 | "type": "string", 220 | "value": "fooValue", 221 | "valueInfo": {}, 222 | }, 223 | } 224 | `; 225 | 226 | exports[`Variables setters setTransient("fooTransient", "fooValueTransient")) should set variable with key "fooTransient" and value "fooValueTransient" and transient "true": dirty variables 1`] = `{}`; 227 | 228 | exports[`Variables setters setTransient("fooTransient", "fooValueTransient")) should set variable with key "fooTransient" and value "fooValueTransient" and transient "true": dirty variables 2`] = ` 229 | { 230 | "fooTransient": { 231 | "type": "string", 232 | "value": "fooValueTransient", 233 | "valueInfo": { 234 | "transient": true, 235 | }, 236 | }, 237 | } 238 | `; 239 | 240 | exports[`Variables setters setTransient("fooTransient", "fooValueTransient")) should set variable with key "fooTransient" and value "fooValueTransient" and transient "true": variables 1`] = `{}`; 241 | 242 | exports[`Variables setters setTransient("fooTransient", "fooValueTransient")) should set variable with key "fooTransient" and value "fooValueTransient" and transient "true": variables 2`] = ` 243 | { 244 | "fooTransient": { 245 | "type": "string", 246 | "value": "fooValueTransient", 247 | "valueInfo": { 248 | "transient": true, 249 | }, 250 | }, 251 | } 252 | `; 253 | 254 | exports[`Variables setters setTyped("baz",someTypeValue) should set typed value with key "baz": dirty variables 1`] = `{}`; 255 | 256 | exports[`Variables setters setTyped("baz",someTypeValue) should set typed value with key "baz": dirty variables 2`] = ` 257 | { 258 | "baz": { 259 | "type": "json", 260 | "value": "{"name":"bazname"}", 261 | "valueInfo": {}, 262 | }, 263 | } 264 | `; 265 | 266 | exports[`Variables setters setTyped("baz",someTypeValue) should set typed value with key "baz": variables 1`] = `{}`; 267 | 268 | exports[`Variables setters setTyped("baz",someTypeValue) should set typed value with key "baz": variables 2`] = ` 269 | { 270 | "baz": { 271 | "type": "json", 272 | "value": { 273 | "name": "bazname", 274 | }, 275 | "valueInfo": {}, 276 | }, 277 | } 278 | `; 279 | -------------------------------------------------------------------------------- /examples/order/assets/order.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | SequenceFlow_0xz1ar4 7 | SequenceFlow_0mbhk8d 8 | 9 | SequenceFlow_1e8m45t 10 | 11 | 12 | 13 | SequenceFlow_131wrxd 14 | 15 | 16 | 17 | SequenceFlow_1e8m45t 18 | SequenceFlow_131wrxd 19 | 20 | 21 | 22 | SequenceFlow_0mbhk8d 23 | 24 | 25 | 26 | SequenceFlow_140kl02 27 | 28 | 29 | 30 | SequenceFlow_1k59pur 31 | SequenceFlow_140kl02 32 | 33 | 34 | 35 | SequenceFlow_0xz1ar4 36 | 37 | R10/PT5S 38 | 39 | 40 | 41 | SequenceFlow_1k59pur 42 | 43 | ${date != null} 44 | 45 | 46 | 47 | date exists? 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 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /lib/__internal/EngineService.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. Camunda licenses this file to you under the Apache License, 6 | * Version 2.0; you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import { jest } from "@jest/globals"; 19 | import got from "got"; 20 | import EngineService from "./EngineService.js"; 21 | import EngineError from "./EngineError.js"; 22 | 23 | describe("EngineService", () => { 24 | let engineService, postSpy, requestSpy; 25 | beforeEach(() => { 26 | engineService = new EngineService({ 27 | workerId: "someWorker", 28 | baseUrl: "some/baseUrl", 29 | }); 30 | postSpy = jest.spyOn(engineService, "post"); 31 | requestSpy = jest.spyOn(engineService, "request"); 32 | }); 33 | 34 | test("post should call request with url and payload", () => { 35 | // given 36 | const expectedUrl = "some/url"; 37 | const expectedPayload = { key: "some value" }; 38 | 39 | // when 40 | engineService.post(expectedUrl, expectedPayload); 41 | 42 | // then 43 | expect(requestSpy).toBeCalledWith("POST", expectedUrl, expectedPayload); 44 | }); 45 | 46 | test("get should call request with url and payload", () => { 47 | // given 48 | const expectedUrl = "some/url"; 49 | const expectedPayload = { key: "some value" }; 50 | 51 | // when 52 | engineService.get(expectedUrl, expectedPayload); 53 | 54 | // then 55 | expect(requestSpy).toBeCalledWith("GET", expectedUrl, expectedPayload); 56 | }); 57 | 58 | test("fetchAndLock", () => { 59 | // given 60 | const expectedUrl = "/external-task/fetchAndLock"; 61 | const expectedReqBody = { someKey: "some value" }; 62 | const expectedPayload = { 63 | json: { ...expectedReqBody, workerId: engineService.workerId }, 64 | }; 65 | 66 | // when 67 | engineService.fetchAndLock(expectedReqBody); 68 | 69 | // then 70 | expect(postSpy).toBeCalledWith(expectedUrl, expectedPayload); 71 | }); 72 | 73 | test("complete", () => { 74 | // given 75 | const expectedTaskId = "foo"; 76 | const expectedUrl = `/external-task/${expectedTaskId}/complete`; 77 | const expectedVariables = { someVariable: "some variable value" }; 78 | const expectedLocalVariables = { 79 | someLocalVariable: "some local variable value", 80 | }; 81 | const expectedPayload = { 82 | json: { 83 | workerId: engineService.workerId, 84 | variables: expectedVariables, 85 | localVariables: expectedLocalVariables, 86 | }, 87 | }; 88 | 89 | // when 90 | engineService.complete({ 91 | id: expectedTaskId, 92 | variables: expectedVariables, 93 | localVariables: expectedLocalVariables, 94 | }); 95 | 96 | // then 97 | expect(postSpy).toBeCalledWith(expectedUrl, expectedPayload); 98 | }); 99 | 100 | test("handleFailure", () => { 101 | // given 102 | const expectedTaskId = "foo"; 103 | const expectedUrl = `/external-task/${expectedTaskId}/failure`; 104 | const expectedRequestBody = { errorMessage: "some error message" }; 105 | const expectedPayload = { 106 | json: { ...expectedRequestBody, workerId: engineService.workerId }, 107 | }; 108 | 109 | // when 110 | engineService.handleFailure({ id: expectedTaskId }, expectedRequestBody); 111 | 112 | // then 113 | expect(postSpy).toBeCalledWith(expectedUrl, expectedPayload); 114 | }); 115 | 116 | test("handleBpmnError", () => { 117 | // given 118 | const expectedTaskId = "foo"; 119 | const expectedUrl = `/external-task/${expectedTaskId}/bpmnError`; 120 | const expectedErrorCode = "some error code"; 121 | const expectedErrorMessage = "some error message"; 122 | const expectedPayload = { 123 | json: { 124 | errorCode: expectedErrorCode, 125 | errorMessage: expectedErrorMessage, 126 | workerId: engineService.workerId, 127 | }, 128 | }; 129 | 130 | // when 131 | engineService.handleBpmnError( 132 | { id: expectedTaskId }, 133 | expectedErrorCode, 134 | expectedErrorMessage 135 | ); 136 | 137 | // then 138 | expect(postSpy).toBeCalledWith(expectedUrl, expectedPayload); 139 | }); 140 | 141 | test("extendLock", () => { 142 | // given 143 | const expectedTaskId = "foo"; 144 | const expectedUrl = `/external-task/${expectedTaskId}/extendLock`; 145 | const expectedNewDuration = 100; 146 | const expectedPayload = { 147 | json: { 148 | newDuration: expectedNewDuration, 149 | workerId: engineService.workerId, 150 | }, 151 | }; 152 | 153 | // when 154 | engineService.extendLock({ id: expectedTaskId }, expectedNewDuration); 155 | 156 | // then 157 | expect(postSpy).toBeCalledWith(expectedUrl, expectedPayload); 158 | }); 159 | 160 | test("unlock", () => { 161 | // given 162 | const expectedTaskId = "foo"; 163 | const expectedUrl = `/external-task/${expectedTaskId}/unlock`; 164 | const expectedPayload = {}; 165 | 166 | // when 167 | engineService.unlock({ id: expectedTaskId }); 168 | 169 | // then 170 | expect(postSpy).toBeCalledWith(expectedUrl, expectedPayload); 171 | }); 172 | 173 | describe("request", () => { 174 | it("should send request with given options", () => { 175 | // given 176 | const method = "POST"; 177 | const path = "/some/url"; 178 | const expectedUrl = `${engineService.baseUrl}${path}`; 179 | const expectedPayload = { 180 | method, 181 | responseType: "buffer", 182 | key: "some value", 183 | }; 184 | 185 | // when 186 | engineService.request(method, path, expectedPayload); 187 | 188 | // then 189 | expect(got).toBeCalledWith(expectedUrl, expectedPayload); 190 | }); 191 | 192 | it("should get request options from interceptors", () => { 193 | // given 194 | const method = "POST"; 195 | const path = "/some/url"; 196 | const expectedUrl = `${engineService.baseUrl}${path}`; 197 | const expectedInitialPayload = { key: "some value" }; 198 | const someExpectedAddedPayload = { someNewKey: "some new value" }; 199 | const anotherExpectedAddedPayload = { 200 | anotherNewKey: "another new value", 201 | }; 202 | const someInterceptor = (config) => ({ 203 | ...config, 204 | ...someExpectedAddedPayload, 205 | }); 206 | const anotherInterceptor = (config) => ({ 207 | ...config, 208 | ...anotherExpectedAddedPayload, 209 | }); 210 | engineService.interceptors = [someInterceptor, anotherInterceptor]; 211 | const expectedPayload = { 212 | method, 213 | ...expectedInitialPayload, 214 | ...someExpectedAddedPayload, 215 | ...anotherExpectedAddedPayload, 216 | responseType: "buffer", 217 | }; 218 | 219 | // when 220 | engineService.request(method, path, expectedPayload); 221 | 222 | // then 223 | expect(got).toBeCalledWith(expectedUrl, expectedPayload); 224 | }); 225 | 226 | it("should throw error if request fails with HTTPError", async () => { 227 | // given 228 | const response = { 229 | body: { 230 | type: "SomeExceptionClass", 231 | message: "a detailed message", 232 | code: 33333, 233 | }, 234 | statusCode: 400, 235 | statusMessage: "Bad request", 236 | }; 237 | const error = new got.HTTPError(response); 238 | error.response = response; 239 | 240 | // then 241 | let thrownError; 242 | 243 | try { 244 | await engineService.request("GET", "", { testResult: error }); 245 | } catch (e) { 246 | thrownError = e; 247 | } 248 | 249 | expect(thrownError).toEqual(new EngineError(error)); 250 | }); 251 | 252 | it("should throw error if request fails with other that HTTPError", async () => { 253 | // given 254 | const error = new got.RequestError(new Error("Some HTTP error"), {}); 255 | 256 | // then 257 | let thrownError; 258 | 259 | try { 260 | await engineService.request("GET", "", { testResult: error }); 261 | } catch (e) { 262 | thrownError = e; 263 | } 264 | 265 | expect(thrownError).toBe(error); 266 | }); 267 | }); 268 | }); 269 | -------------------------------------------------------------------------------- /docs/handler.md: -------------------------------------------------------------------------------- 1 | # Handler Function 2 | 3 | The handler function is used to handle a fetched task. 4 | 5 | ```js 6 | const { Client, logger, Variables } = require("camunda-external-task-client-js"); 7 | 8 | // create a Client instance with custom configuration 9 | const client = new Client({ baseUrl: "http://localhost:8080/engine-rest", use: logger }); 10 | 11 | // create a handler function 12 | const handler = async function({ task, taskService }) { 13 | // get the process variable 'score' 14 | const score = Variables.get("score"); 15 | // set a process variable 'winning' 16 | const processVariables = new Variables().set("winning", score > 5); 17 | // set a local variable 'winningDate' 18 | const localVariables = new Variables().set("winningDate", new Date()); 19 | 20 | // complete the task 21 | await taskService.complete(task, processVariables, localVariables); 22 | }; 23 | 24 | // susbscribe to the topic: 'topicName' 25 | client.subscribe("topicName", handler); 26 | ``` 27 | 28 | ## `task.variables` 29 | 30 | The `variables` object is a read-only [Variables](/docs/Variables) instance. 31 | It provides various [getters](/docs/Variables#variablesgetvariablename) 32 | for reading the process variables in the scope of the service task. 33 | 34 | ```js 35 | client.subscribe("bar", async function({ task, taskService }) { 36 | // output all process variables 37 | console.log(task.variables.getAll()); 38 | }); 39 | ``` 40 | ## `taskService.complete(task, processVariables, localVariables)` 41 | 42 | | Parameter | Description | Type | Required | 43 | |------------------|----------------------------------------------|------------------|----------| 44 | | task | task or id of the task to complete | object or string | ✓ | 45 | | processVariables | map of variables to set in the process scope | object | | 46 | | localVariables | map of variables to set in the task scope | object | | 47 | 48 | ```js 49 | // Susbscribe to the topic: 'topicName' 50 | client.subscribe("topicName", async function({ task, taskService }) { 51 | // Put your business logic 52 | 53 | // Complete the task 54 | await taskService.complete(task); 55 | }); 56 | ``` 57 | 58 | ## `taskService.handleFailure(task, options)` 59 | 60 | | Parameter | Description | Type | Required | 61 | |-----------|------------------------------------------|------------------|----------| 62 | | task | task or id of the task to handle failure. | object or string | ✓ | 63 | | options | options about the failure. | object | | 64 | 65 | 66 | options include: 67 | 68 | | Option | Description | Type | Required | Default | 69 | |--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------|----------|---------| 70 | | errorMessage | An message indicating the reason of the failure. | string | | | 71 | | errorDetails | A detailed error description. | string | | | 72 | | retries | A number of how often the task should be retried. Must be >= 0. If this is 0, an incident is created and the task cannot be fetched anymore unless the retries are increased again. The incident's message is set to the errorMessage parameter. | number | | | 73 | | retryTimeout | A timeout in milliseconds before the external task becomes available again for fetching. Must be >= 0. | number | | | 74 | 75 | ```js 76 | // Susbscribe to the topic: 'topicName' 77 | client.subscribe("topicName", async function({ task, taskService }) { 78 | // Put your business logic 79 | 80 | // Handle a Failure 81 | await taskService.handleFailure(task, { 82 | errorMessage: "some failure message", 83 | errorDetails: "some details", 84 | retries: 1, 85 | retryTimeout: 1000 86 | }); 87 | }); 88 | ``` 89 | 90 | ## `taskService.handleBpmnError(task, errorCode)` 91 | | Parameter | Description | Type | Required | 92 | |--------------|------------------------------------------------------------------------------------------------|------------------|----------| 93 | | task | task or id of the task to handle bpmn failure | object or string | ✓ | 94 | | errorCode | An error code that indicates the predefined error. Is used to identify the BPMN error handler. | string | ✓ | 95 | | errorMessage | An error message that describes the error. | string | | 96 | | variables | Map of variables which will be passed to the execution. | | | 97 | 98 | ```js 99 | // Susbscribe to the topic: 'topicName' 100 | client.subscribe("topicName", async function({ task, taskService }) { 101 | // Put your business logic 102 | 103 | // Create some variables 104 | const variables = new Variables().set('date', new Date()); 105 | 106 | // Handle a BPMN Failure 107 | await taskService.handleBpmnError(task, "BPMNError_Code", "Error message", variables); 108 | }); 109 | ``` 110 | 111 | ## `taskService.extendLock(task, newDuration)` 112 | 113 | | Parameter | Description | Type | Required | 114 | |-------------|------------------------------------------------------------------------------------------------------|------------------|----------| 115 | | task | task or id of the task to extend lock duration | object or string | ✓ | 116 | | newDuration | An amount of time (in milliseconds). This is the new lock duration starting from the current moment. | number | ✓ | 117 | 118 | ```js 119 | // Susbscribe to the topic: 'topicName' 120 | client.subscribe("topicName", async function({ task, taskService }) { 121 | // Put your business logic 122 | 123 | // Extend the lock time 124 | try { 125 | await taskService.extendLock(task, 5000); 126 | console.log("I extended the lock time successfully!!"); 127 | } catch (e) { 128 | console.error(`Failed to extend the lock time, ${e}`); 129 | } 130 | }); 131 | ``` 132 | 133 | ## `taskService.unlock(task)` 134 | 135 | | Parameter | Description | Type | Required | 136 | |-----------|----------------------------------|------------------|----------| 137 | | task | task or id of the task to unlock | object or string | ✓ | 138 | 139 | ```js 140 | // Susbscribe to the topic: 'topicName' 141 | client.subscribe("topicName", async function({ task, taskService }) { 142 | // Put your business logic 143 | 144 | // Unlock the task 145 | await taskService.unlock(task); 146 | }); 147 | ``` 148 | 149 | ## `taskService.lock(task, newDuration)` 150 | 151 | | Parameter | Description | Type | Required | 152 | |-------------|--------------------------------------------------------------------------------------------------|------------------|----------| 153 | | task | task or id of the task to extend lock duration | object or string | ✓ | 154 | | duration | An amount of time (in milliseconds). This is the lock duration starting from the current moment. | number | ✓ | 155 | 156 | ```js 157 | // Susbscribe to the topic: 'topicName' 158 | client.subscribe("topicName", async function({ task, taskService }) { 159 | // Task is locked by default 160 | // Put your business logic, unlock the task or let the lock expire 161 | 162 | // Lock a task again 163 | try { 164 | await taskService.lock(task, 5000); 165 | console.log("I locked the task successfully!"); 166 | } catch (e) { 167 | console.error(`Failed to lock the task, ${e}`); 168 | } 169 | }); 170 | ``` -------------------------------------------------------------------------------- /test/test-process.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SequenceFlow_0r8ikrt 6 | SequenceFlow_0g8kjqd 7 | 8 | 9 | SequenceFlow_1fcfgp0 10 | SequenceFlow_1xfvjkv 11 | 12 | 13 | SequenceFlow_0g8kjqd 14 | 15 | 16 | SequenceFlow_1xfvjkv 17 | 18 | 19 | SequenceFlow_1161usx 20 | SequenceFlow_0r8ikrt 21 | SequenceFlow_1fcfgp0 22 | 23 | 24 | 25 | 26 | 27 | 28 | 0}]]> 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 40 | 41 | 42 | 43 | SequenceFlow_1349btf 44 | SequenceFlow_1161usx 45 | 46 | 47 | SequenceFlow_1349btf 48 | 49 | 50 | Topic name: creditScoreChecker 51 | 52 | 53 | 54 | Topic name: loanGranter 55 | 56 | 57 | 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 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /lib/__internal/utils.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH 3 | * under one or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information regarding copyright 5 | * ownership. Camunda licenses this file to you under the Apache License, 6 | * Version 2.0; you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import { jest } from "@jest/globals"; 19 | import { 20 | isFunction, 21 | andArrayWith, 22 | isArrayOfFunctions, 23 | isUndefinedOrNull, 24 | getVariableType, 25 | mapEntries, 26 | deserializeVariable, 27 | serializeVariable, 28 | } from "./utils.js"; 29 | 30 | import File from "../File"; 31 | 32 | describe("utils", () => { 33 | describe("isFunction", () => { 34 | it("should return false if param is not a function", () => { 35 | expect(isFunction()).toBe(false); 36 | expect(isFunction(2)).toBe(false); 37 | }); 38 | 39 | it("should return true if param is a function", () => { 40 | expect(isFunction(() => {})).toBe(true); 41 | }); 42 | }); 43 | 44 | describe("andArrayWith", () => { 45 | it("should apply test function on each element on the array and AND the results", () => { 46 | const biggerThan5 = (a) => a > 5; 47 | const arr1 = [1, 2, 3, 4]; 48 | const arr2 = [6, 7, 8, 9]; 49 | const arr3 = [6, 7, 2, 8, 9]; 50 | 51 | expect(andArrayWith(arr1, biggerThan5)).toBe(false); 52 | expect(andArrayWith(arr2, biggerThan5)).toBe(true); 53 | expect(andArrayWith(arr3, biggerThan5)).toBe(false); 54 | }); 55 | }); 56 | 57 | describe("isArrayOfFunctions", () => { 58 | it("should return false for non array", () => { 59 | expect(isArrayOfFunctions(3)).toBe(false); 60 | }); 61 | 62 | it("should return false for non array of functions", () => { 63 | expect(isArrayOfFunctions([1, 2])).toBe(false); 64 | }); 65 | 66 | it("should return true for an array of functions", () => { 67 | expect(isArrayOfFunctions([() => {}, () => {}])).toBe(true); 68 | }); 69 | }); 70 | 71 | describe("isUndefinedOrNull", () => { 72 | it("should return false for non undefined or null", () => { 73 | expect(isUndefinedOrNull(1)).toBe(false); 74 | expect(isUndefinedOrNull("foo")).toBe(false); 75 | expect(isUndefinedOrNull([])).toBe(false); 76 | expect(isUndefinedOrNull(() => {})).toBe(false); 77 | }); 78 | 79 | it("should return true for undefined", () => { 80 | expect(isUndefinedOrNull(undefined)).toBe(true); 81 | }); 82 | 83 | it("should return true for null", () => { 84 | expect(isUndefinedOrNull(null)).toBe(true); 85 | }); 86 | }); 87 | 88 | describe("getVariableType", () => { 89 | test("getVariableType(null) should be null", () => { 90 | expect(getVariableType(null)).toBe("null"); 91 | }); 92 | 93 | test("getVariableType() should be null", () => { 94 | expect(getVariableType()).toBe("null"); 95 | }); 96 | 97 | test("getVariableType(1) should be integer", () => { 98 | expect(getVariableType(1)).toBe("integer"); 99 | }); 100 | 101 | test("getVariableType(2^32) should be long", () => { 102 | expect(getVariableType(Math.pow(2, 32))).toBe("long"); 103 | }); 104 | 105 | test("getVariableType(2.32) should be double", () => { 106 | expect(getVariableType(2.32)).toBe("double"); 107 | }); 108 | 109 | test("getVariableType(true) should be boolean", () => { 110 | expect(getVariableType(true)).toBe("boolean"); 111 | }); 112 | 113 | test("getVariableType('foo') should be string", () => { 114 | expect(getVariableType("foo")).toBe("string"); 115 | }); 116 | 117 | test("getVariableType(new File) should be file", () => { 118 | const file = new File({ localPath: "foo" }); 119 | expect(getVariableType(file)).toBe("file"); 120 | }); 121 | 122 | test('getVariableType({"x": 2}) should be json', () => { 123 | expect(getVariableType({ x: 2 })).toBe("json"); 124 | }); 125 | 126 | test("getVariableType({ x: 2 }) should be json", () => { 127 | expect(getVariableType({ x: 2 })).toBe("json"); 128 | }); 129 | }); 130 | 131 | describe("mapEntries", () => { 132 | it("should map entries with mapper: entry -> entry × 2", () => { 133 | // given 134 | const initialObject = { a: 2, b: 3, c: 4 }; 135 | const expectedObject = { a: 4, b: 6, c: 8 }; 136 | const mapper = ({ key, value }) => ({ [key]: value * 2 }); 137 | 138 | // then 139 | expect(mapEntries(initialObject, mapper)).toEqual(expectedObject); 140 | }); 141 | }); 142 | 143 | describe("deserializeVariable", () => { 144 | it("value should remain the same if type is neither file, json nor date", () => { 145 | // given 146 | let typedValue = { value: "some value", type: "string", valueInfo: {} }; 147 | 148 | // then 149 | expect(deserializeVariable({ typedValue })).toMatchObject(typedValue); 150 | }); 151 | 152 | it("value should be parsed if type is JSON", () => { 153 | // given 154 | let parsedValue = { x: 10 }; 155 | let typedValue = { 156 | value: JSON.stringify(parsedValue), 157 | type: "json", 158 | valueInfo: {}, 159 | }; 160 | let expectedTypedValue = { ...typedValue, value: parsedValue }; 161 | 162 | // then 163 | expect(deserializeVariable({ typedValue })).toMatchObject( 164 | expectedTypedValue 165 | ); 166 | }); 167 | 168 | it("value should be a File instance if type is file", () => { 169 | // given 170 | let typedValue = { value: "", type: "File", valueInfo: {} }; 171 | let options = { 172 | key: "variable_key", 173 | typedValue, 174 | processInstanceId: "process_instance_id", 175 | engineService: {}, 176 | }; 177 | let result = deserializeVariable(options); 178 | 179 | // then 180 | // the value should be a file 181 | expect(result.value).toBeInstanceOf(File); 182 | 183 | // match file parameters to snapshot 184 | expect(result).toMatchSnapshot(); 185 | }); 186 | 187 | it("value should become a Date object if type is date", () => { 188 | // given 189 | let dateStr = "2013-06-30T21:04:22.000+0200"; 190 | let dateObj = new Date(dateStr); 191 | let typedValue = { 192 | value: dateStr, 193 | type: "Date", 194 | valueInfo: {}, 195 | }; 196 | 197 | // then 198 | expect(deserializeVariable({ typedValue }).value).toMatchObject(dateObj); 199 | }); 200 | }); 201 | 202 | describe("serializeVariable", () => { 203 | it("value should remain the same if type is neither file, json nor date", () => { 204 | // given 205 | let typedValue = { value: 21, type: "integer", valueInfo: {} }; 206 | 207 | // then 208 | expect(serializeVariable({ typedValue })).toMatchObject(typedValue); 209 | }); 210 | 211 | it("value should be stringifyed if type is JSON and value is not a string", () => { 212 | // given 213 | let parsedValue = { x: 10 }; 214 | let typedValue = { 215 | value: parsedValue, 216 | type: "json", 217 | valueInfo: {}, 218 | }; 219 | let expectedTypedValue = { 220 | ...typedValue, 221 | value: JSON.stringify(parsedValue), 222 | }; 223 | 224 | // then 225 | expect(serializeVariable({ typedValue })).toMatchObject( 226 | expectedTypedValue 227 | ); 228 | }); 229 | 230 | it("value should remain the same if type is JSON and value is a string", () => { 231 | // given 232 | let value = JSON.stringify({ x: 10 }); 233 | let typedValue = { 234 | value, 235 | type: "json", 236 | valueInfo: {}, 237 | }; 238 | 239 | // then 240 | expect(serializeVariable({ typedValue })).toMatchObject(typedValue); 241 | }); 242 | 243 | it("should return result of createTypedValue if instance if type is file and value is an instance of File", () => { 244 | // given 245 | let value = new File({ localPath: "some/path" }); 246 | let createTypedValueSpy = jest.spyOn(value, "createTypedValue"); 247 | let typedValue = { value, type: "File", valueInfo: {} }; 248 | let result = serializeVariable({ typedValue }); 249 | 250 | // then 251 | // createTypedValue should be called 252 | expect(createTypedValueSpy).toBeCalled(); 253 | 254 | // result must be the result of createTypedValue 255 | expect(result).toMatchObject(value.createTypedValue()); 256 | }); 257 | 258 | it("value should remain the same if instance if type is file and value is not an instance of File", () => { 259 | // given 260 | let value = "some value"; 261 | let typedValue = { value, type: "file", valueInfo: {} }; 262 | let result = serializeVariable({ typedValue }); 263 | 264 | // then 265 | expect(result).toMatchObject(typedValue); 266 | }); 267 | 268 | it("value should be converted to proper formatted string if type is date and value is an instance of date", () => { 269 | // given 270 | let dateStr = "2013-06-30T21:04:22.000+0200"; 271 | let formattedDate = "2013-06-30T19:04:22.000+0000"; 272 | let dateObj = new Date(dateStr); 273 | let typedValue = { 274 | value: dateObj, 275 | type: "Date", 276 | valueInfo: {}, 277 | }; 278 | 279 | // then 280 | expect(serializeVariable({ typedValue }).value).toBe(formattedDate); 281 | }); 282 | }); 283 | }); 284 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # camunda-external-task-client 2 | 3 | [![npm version](https://badge.fury.io/js/camunda-external-task-client-js.svg)](https://badge.fury.io/js/camunda-external-task-client-js) 4 | ![CI](https://github.com/camunda/camunda-external-task-client-js/actions/workflows/CI.yml/badge.svg) 5 | 6 | Implement your [BPMN Service Task](https://docs.camunda.org/manual/latest/user-guide/process-engine/external-tasks/) in 7 | NodeJS. 8 | 9 | > This package is an [ECMAScript module](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) (ESM) and provides no CommonJS exports. 10 | 11 | > NodeJS >= v18 is required 12 | 13 | ## Installing 14 | 15 | ```sh 16 | npm install -s camunda-external-task-client-js 17 | ``` 18 | 19 | Or: 20 | 21 | ```sh 22 | yarn add camunda-external-task-client-js 23 | ``` 24 | 25 | ## Usage 26 | 27 | 1. Make sure to have [Camunda](https://camunda.com/download/) running. 28 | 2. Create a simple process model with an External Service Task and define the topic as 'topicName'. 29 | 3. Deploy the process to the Camunda Platform engine. 30 | 4. In your NodeJS script: 31 | 32 | ```js 33 | import { Client, logger } from "camunda-external-task-client-js"; 34 | 35 | // configuration for the Client: 36 | // - 'baseUrl': url to the Process Engine 37 | // - 'logger': utility to automatically log important events 38 | const config = { baseUrl: "http://localhost:8080/engine-rest", use: logger }; 39 | 40 | // create a Client instance with custom configuration 41 | const client = new Client(config); 42 | 43 | // susbscribe to the topic: 'creditScoreChecker' 44 | client.subscribe("creditScoreChecker", async function({ task, taskService }) { 45 | // Put your business logic 46 | // complete the task 47 | await taskService.complete(task); 48 | }); 49 | ``` 50 | 51 | > **Note:** Although the examples used in this documentation use _async await_ for handling asynchronous calls, you 52 | > can also use Promises to achieve the same results. 53 | 54 | ## About External Tasks 55 | 56 | External Tasks are service tasks whose execution differs particularly from the execution of other service tasks (e.g. Human Tasks). 57 | The execution works in a way that units of work are polled from the engine before being completed. 58 | 59 | **camunda-external-task-client.js** allows you to create easily such client in NodeJS. 60 | 61 | ## Features 62 | 63 | ### [Fetch and Lock](https://docs.camunda.org/manual/latest/reference/rest/external-task/fetch/) 64 | 65 | Done through [polling](/docs/Client.md#about-polling). 66 | 67 | ### [Complete](https://docs.camunda.org/manual/latest/reference/rest/external-task/post-complete/) 68 | 69 | ```js 70 | // Susbscribe to the topic: 'topicName' 71 | client.subscribe("topicName", async function({ task, taskService }) { 72 | // Put your business logic 73 | // Complete the task 74 | await taskService.complete(task); 75 | }); 76 | ``` 77 | 78 | ### [Handle Failure](https://docs.camunda.org/manual/latest/reference/rest/external-task/post-failure/) 79 | 80 | ```js 81 | // Susbscribe to the topic: 'topicName' 82 | client.subscribe("topicName", async function({ task, taskService }) { 83 | // Put your business logic 84 | // Handle a Failure 85 | await taskService.handleFailure(task, { 86 | errorMessage: "some failure message", 87 | errorDetails: "some details", 88 | retries: 1, 89 | retryTimeout: 1000 90 | }); 91 | 92 | }); 93 | ``` 94 | 95 | ### [Handle BPMN Error](https://docs.camunda.org/manual/latest/reference/rest/external-task/post-bpmn-error/) 96 | 97 | ```js 98 | // Susbscribe to the topic: 'topicName' 99 | client.subscribe("topicName", async function({ task, taskService }) { 100 | // Put your business logic 101 | 102 | // Create some variables 103 | const variables = new Variables().set('date', new Date()); 104 | 105 | // Handle a BPMN Failure 106 | await taskService.handleBpmnError(task, "BPMNError_Code", "Error message", variables); 107 | }); 108 | ``` 109 | 110 | ### [Extend Lock](https://docs.camunda.org/manual/latest/reference/rest/external-task/post-extend-lock/) 111 | 112 | ```js 113 | // Susbscribe to the topic: 'topicName' 114 | client.subscribe("topicName", async function({ task, taskService }) { 115 | // Put your business logic 116 | // Extend the lock time 117 | await taskService.extendLock(task, 5000); 118 | }); 119 | ``` 120 | 121 | ### [Unlock](https://docs.camunda.org/manual/latest/reference/rest/external-task/post-unlock/) 122 | 123 | ```js 124 | // Susbscribe to the topic: 'topicName' 125 | client.subscribe("topicName", async function({ task, taskService }) { 126 | // Put your business logic 127 | // Unlock the task 128 | await taskService.unlock(task); 129 | }); 130 | ``` 131 | 132 | ### [Lock](https://docs.camunda.org/manual/latest/reference/rest/external-task/post-lock/) 133 | ```js 134 | // Susbscribe to the topic: 'topicName' 135 | client.subscribe("topicName", async function({ task, taskService }) { 136 | // Task is locked by default 137 | // Put your business logic, unlock the task or let the lock expire 138 | 139 | // Lock a task again 140 | await taskService.lock(task, 5000); 141 | }); 142 | ``` 143 | 144 | ### Exchange Process & Local Task Variables 145 | 146 | ```js 147 | import { Variables } from "camunda-external-task-client-js"; 148 | 149 | client.subscribe("topicName", async function({ task, taskService }) { 150 | // get the process variable 'score' 151 | const score = task.variables.get("score"); 152 | 153 | // set a process variable 'winning' 154 | const processVariables = new Variables(); 155 | processVariables.set("winning", score > 5); 156 | 157 | // set a local variable 'winningDate' 158 | const localVariables = new Variables(); 159 | localVariables.set("winningDate", new Date()); 160 | 161 | // complete the task 162 | await taskService.complete(task, processVariables, localVariables); 163 | }); 164 | ``` 165 | 166 | ## API 167 | 168 | * [Client](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/Client.md) 169 | * [new Client(options)](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/Client.md#new-clientoptions) 170 | * [client.start()](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/Client.md#clientstart) 171 | * [client.subscribe(topic, [options], handler)](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/Client.md#clientsubscribetopic-options-handler) 172 | * [client.stop()](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/Client.md#clientstop) 173 | * [Client Events)](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/Client.md#client-events) 174 | * [About the Handler Function](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/handler.md) 175 | * [Variables](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/Variables.md) 176 | * [new Variables(options)](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/Variables.md#new-variablesoptions") 177 | * [variables.get(variableName)](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/Variables.md#variablesgetvariablename) 178 | * [variables.getTyped(variableName)](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/Variables.md#variablesgettypedvariablename) 179 | * [variables.getAll()](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/Variables.md#variablesgetall) 180 | * [variables.getAllTyped()](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/Variables.md#variablesgetalltyped) 181 | * [variables.set(variableName)](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/Variables.md#variablessetvariablename-value) 182 | * [variables.setTyped(variableName)](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/Variables.md#variablessettypedvariablename-typedvalue) 183 | * [variables.setAll(values)](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/Variables.md#variablessetallvalues) 184 | * [variables.setAllTyped(typedValues)](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/Variables.md#variablessetalltypedtypedvalues) 185 | * [About JSON & Date Variables](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/Variables.md#about-json--date-variables) 186 | * [File](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/File.md) 187 | * [new File(options)](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/File.md#new-fileoptions) 188 | * [file.load()](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/File.md#fileload) 189 | * [File Properties](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/File.md#file-properties) 190 | * [BasicAuthInterceptor](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/BasicAuthInterceptor.md) 191 | * [new BasicAuthInterceptor(options)](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/BasicAuthInterceptor.md#new-basicauthinterceptoroptions) 192 | * [KeycloakAuthInterceptor](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/KeycloakAuthInterceptor.md) 193 | * [new KeycloakAuthInterceptor(options)](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/KeycloakAuthInterceptor.md#new-keycloakauthinterceptoroptions) 194 | * [logger](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/logger.md) 195 | * [logger.success(text)](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/logger.md#loggersuccesstext) 196 | * [logger.error(text)](https://github.com/camunda/camunda-external-task-client-js/blob/master/docs/logger.md#loggererrortext) 197 | 198 | ## Contributing 199 | 200 | Have a look at our [contribution guide](https://github.com/camunda/camunda-bpm-platform/blob/master/CONTRIBUTING.md) for how to contribute to this repository. 201 | 202 | ## Help and support 203 | 204 | * [Documentation](https://docs.camunda.org/manual/latest/) 205 | * [Forum](https://forum.camunda.org) 206 | * [Stackoverflow](https://stackoverflow.com/questions/tagged/camunda) 207 | 208 | ## License 209 | 210 | The source files in this repository are made available under the [Apache License Version 2.0](./LICENSE). 211 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /docs/Client.md: -------------------------------------------------------------------------------- 1 | # `Client` 2 | 3 | ```js 4 | const { Client } = require("camunda-external-task-handler-js"); 5 | 6 | const client = new Client({baseUrl: "http://localhost:8080/engine-rest"}); 7 | 8 | client.subscribe("foo", async function({task, taskService}) { 9 | // Put your business logic 10 | }); 11 | ``` 12 | 13 | Client is the core class of the external task client. 14 | It is mainly used to start/stop the external task client and subscribe to a certain topic. 15 | 16 | ## `new Client(options)` 17 | 18 | Options are **mandatory** when creating a _Client_ instance. 19 | 20 | Here"s a list of the available options: 21 | 22 | | Option | Description | Type | Required | Default | 23 | |:--------------------:|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|------------------------|:--------:|:----------------:| 24 | | baseUrl | Path to the engine api | string | ✓ | | 25 | | workerId | The id of the worker on which behalf tasks are fetched. The returned tasks are locked for that worker and can only be completed when providing the same worker id. | string | | "some-random-id" | 26 | | maxTasks | The maximum number of tasks to fetch | number | | 10 | 27 | | maxParallelExecutions | The maximum number of tasks to be worked on simultaneously | number | | | 28 | | interval | Interval of time to wait before making a new poll. | number | | 300 | 29 | | lockDuration | The default duration to lock the external tasks for in milliseconds. | number | | 50000 | 30 | | autoPoll | If true, then polling start automatically as soon as a Client instance is created. | boolean | | true | 31 | | asyncResponseTimeout | The Long Polling timeout in milliseconds. | number | | | 32 | | usePriority | If false, task will be fetched arbitrarily instead of based on its priority. | boolean | | true | 33 | | interceptors | Function(s) that will be called before a request is sent. Interceptors receive the configuration of the request and return a new configuration. | function or [function] | | | 34 | | use | Function(s) that have access to the client instance as soon as it is created and before any polling happens. Check out [logger](/lib/logger.js) for a better understanding of the usage of middlewares. | function or [function] | | | 35 | | sorting | Defines the sorting of the fetched tasks. It can be used together with `usePriority`, but the sorting will be based on priority first. | Array of `Sorting` objects | | | 36 | 37 | `Sorting` object properties: 38 | 39 | | Option | Description | Type | Required | Default | 40 | |:---------:|:-------------------------------------------------------------------------:|--------|:--------:|:-------:| 41 | | sortBy | Specifies the sorting by property. [Possible values.](/lib/Client.js#L67) | string | ✓ | | 42 | | sortOrder | Specifies the sorting direction. [Possible values.](/lib/Client.js#L71) | string | ✓ | | 43 | 44 | ### About interceptors 45 | 46 | * Interceptors receive the configuration of the request and return a new configuration. 47 | * In the case of multiple interceptors, they are piped in the order they are provided. 48 | * Check out [BasicAuthInterceptor](/lib/BasicAuthInterceptor.js) for a better understanding of the usage of interceptors. 49 | 50 | ## `client.start()` 51 | 52 | Triggers polling. 53 | 54 | ### About Polling 55 | 56 | * Polling tasks from the engine works by performing a fetch & lock operation of tasks that have subscriptions. It then calls the handler registered to each task. 57 | * Polling is done periodically based on the _interval_ configuration. 58 | * [Long Polling](https://docs.camunda.org/manual/latest/user-guide/process-engine/external-tasks/#long-polling-to-fetch-and-lock-external-tasks) is enabled by configuring the option _asyncResponseTimeout_. 59 | 60 | ## `client.subscribe(topic, options, handler)` 61 | 62 | Subscribes a handler to a specific topic and returns a _topic subscription_. 63 | Here"s a list of the available parameters: 64 | 65 | | Parameter | Description | Type | Required | 66 | | --------- | ----------------------------------------------------- | -------- | -------- | 67 | | topic | topic name for which external tasks should be fetched | string | ✓ | 68 | | options | options about subscription | object | | 69 | | handler | function to handle fetched task. Checkout [this](/docs/handler.md) for more information | function | ✓ | 70 | 71 | The currently supported options are: 72 | 73 | | Option | Description | Type | Required | Default | 74 | |--------------|-------------------------------------------------------------------------------------|--------|----------|-------------------------------------------------------| 75 | | lockDuration | specifies the lock duration for this specific handler. | number | | global lockDuration configured in the client instance | 76 | | variables | defines a subset of variables available in the handler. | array | | | 77 | | businessKey | A value which allows to filter tasks based on process instance business key | string | | | 78 | | processDefinitionId | A value which allows to filter tasks based on process definition id | string | | | 79 | | processDefinitionIdIn | A value which allows to filter tasks based on process definition ids | string | | | 80 | | processDefinitionKey | A value which allows to filter tasks based on process definition key | string | | | 81 | | processDefinitionKeyIn | A value which allows to filter tasks based on process definition keys | string | | | 82 | | processDefinitionVersionTag | A value which allows to filter tasks based on process definition Version Tag | string | | 83 | | processVariables | A JSON object used for filtering tasks based on process instance variable values. A property name of the object represents a process variable name, while the property value represents the process variable value to filter tasks by. | object | | | 84 | | tenantIdIn | A value which allows to filter tasks based on tenant ids | string | | | 85 | | withoutTenantId | A value which allows to filter tasks without tenant id | boolean | | | 86 | | localVariables | A value which allow to fetch only local variables | boolean | | | 87 | | includeExtensionProperties | A value which allow to fetch `extensionProperties` | boolean | | | 88 | 89 | 90 | ### About topic subscriptions 91 | 92 | A topic subscription, which is returned by the **subscribe()** method, is a an object that provides the following: 93 | 94 | * **handler:** a function that is executed whenever a task is fetched & locked for the topic subscribed to. 95 | * **unsubscribe():** a function to unsubscribe from a topic. 96 | * **lockDuration:** the configured lockDuration for this specific topic subscription. 97 | * **variables:** the selected subset of variables. 98 | 99 | ```js 100 | const {Client} = require("camunda-external-task-client-js"); 101 | 102 | const client = new Client({baseUrl: "http://localhost:8080/engine-rest"}); 103 | 104 | const topicSubscription = client.subscribe( 105 | "foo", 106 | { 107 | // Put your options here 108 | processDefinitionVersionTag: "v2" 109 | }, 110 | async function({task, taskService}) { 111 | // Put your business logic 112 | } 113 | ); 114 | 115 | // unsubscribe from a topic 116 | topicSubscription.unsubscribe(); 117 | ``` 118 | 119 | ## `client.stop()` 120 | 121 | Stops polling. 122 | 123 | ## Client Events 124 | 125 | > Check out [logger](/lib/logger.js) for a better understanding of the usage of events. 126 | > Here"s a list of available client events: 127 | 128 | * `client.on("subscribe", function(topic, topicSubscription) {})` 129 | * `client.on("unsubscribe", function(topic, topicSubscription) {})` 130 | * `client.on("poll:start", function() {})` 131 | * `client.on("poll:stop", function() {})` 132 | * `client.on("poll:success", function(tasks) {})` 133 | * `client.on("poll:error", function(error) {})` 134 | * `client.on("complete:success", function(task) {})` 135 | * `client.on("complete:error", function(task, error) {})` 136 | * `client.on("handleFailure:success", function(task) {})` 137 | * `client.on("handleFailure:error", function(task, error) {})` 138 | * `client.on("handleBpmnError:success", function(task) {})` 139 | * `client.on("handleBpmnError:error", function(task, error) {})` 140 | * `client.on("extendLock:success", function(task) {})` 141 | * `client.on("extendLock:error", function(task, error) {})` 142 | * `client.on("unlock:success", function(task) {})` 143 | * `client.on("unlock:error", function(task, error) {})` 144 | * `client.on("lock:success", function(task) {})` 145 | * `client.on("lock:error", function(task, error) {})` 146 | 147 | ### Error 148 | 149 | The error object exposes the response body of the REST API request and has the following properties: 150 | 151 | | Property | Description | Type | 152 | |----------------|------------------------------------------------------------------------------------------------------------------------------------|--------| 153 | | message | A summary of the error, e.g., `Response code 400 (Bad Request); Error: my engine error; Type: ProcessEngineException; Code: 33333` | string | 154 | | httpStatusCode | The HTTP status code returned by the REST API, e.g., `400` | number | 155 | | engineMsg | The engine error message, e.g., `my engine error` | string | 156 | | type | The class name of the exception, e.g., `ProcessEngineException` | string | 157 | | code | A numeric exception error code, e.g., `33333`. | number | 158 | 159 | You can find more information about the exception error code feature in the [Camunda Platform 7 Documentation](https://docs.camunda.org/manual/latest/user-guide/process-engine/error-handling/#exception-codes). -------------------------------------------------------------------------------- /examples/order/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "order", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "order", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "camunda-external-task-client-js": "3.1.0" 13 | } 14 | }, 15 | "node_modules/@sindresorhus/is": { 16 | "version": "6.2.0", 17 | "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-6.2.0.tgz", 18 | "integrity": "sha512-yM/IGPkVnYGblhDosFBwq0ZGdnVSBkNV4onUtipGMOjZd4kB6GAu3ys91aftSbyMHh6A2GPdt+KDI5NoWP63MQ==", 19 | "engines": { 20 | "node": ">=16" 21 | }, 22 | "funding": { 23 | "url": "https://github.com/sindresorhus/is?sponsor=1" 24 | } 25 | }, 26 | "node_modules/@szmarczak/http-timer": { 27 | "version": "5.0.1", 28 | "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", 29 | "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", 30 | "dependencies": { 31 | "defer-to-connect": "^2.0.1" 32 | }, 33 | "engines": { 34 | "node": ">=14.16" 35 | } 36 | }, 37 | "node_modules/@types/http-cache-semantics": { 38 | "version": "4.0.4", 39 | "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", 40 | "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==" 41 | }, 42 | "node_modules/cacheable-lookup": { 43 | "version": "7.0.0", 44 | "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", 45 | "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", 46 | "engines": { 47 | "node": ">=14.16" 48 | } 49 | }, 50 | "node_modules/cacheable-request": { 51 | "version": "10.2.14", 52 | "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", 53 | "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", 54 | "dependencies": { 55 | "@types/http-cache-semantics": "^4.0.2", 56 | "get-stream": "^6.0.1", 57 | "http-cache-semantics": "^4.1.1", 58 | "keyv": "^4.5.3", 59 | "mimic-response": "^4.0.0", 60 | "normalize-url": "^8.0.0", 61 | "responselike": "^3.0.0" 62 | }, 63 | "engines": { 64 | "node": ">=14.16" 65 | } 66 | }, 67 | "node_modules/cacheable-request/node_modules/get-stream": { 68 | "version": "6.0.1", 69 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", 70 | "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", 71 | "engines": { 72 | "node": ">=10" 73 | }, 74 | "funding": { 75 | "url": "https://github.com/sponsors/sindresorhus" 76 | } 77 | }, 78 | "node_modules/camunda-external-task-client-js": { 79 | "version": "3.1.0", 80 | "resolved": "https://registry.npmjs.org/camunda-external-task-client-js/-/camunda-external-task-client-js-3.1.0.tgz", 81 | "integrity": "sha512-x576bN86Seipz+Z5/1E99mf227D4l4U2OGYa3NTxE0E9Sywomg5a8ozc5XSHt20jyXp96CBY53psI19YCh8nHA==", 82 | "dependencies": { 83 | "chalk": "^5.3.0", 84 | "got": "^14.2.0" 85 | } 86 | }, 87 | "node_modules/chalk": { 88 | "version": "5.3.0", 89 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", 90 | "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", 91 | "engines": { 92 | "node": "^12.17.0 || ^14.13 || >=16.0.0" 93 | }, 94 | "funding": { 95 | "url": "https://github.com/chalk/chalk?sponsor=1" 96 | } 97 | }, 98 | "node_modules/decompress-response": { 99 | "version": "6.0.0", 100 | "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", 101 | "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", 102 | "dependencies": { 103 | "mimic-response": "^3.1.0" 104 | }, 105 | "engines": { 106 | "node": ">=10" 107 | }, 108 | "funding": { 109 | "url": "https://github.com/sponsors/sindresorhus" 110 | } 111 | }, 112 | "node_modules/decompress-response/node_modules/mimic-response": { 113 | "version": "3.1.0", 114 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", 115 | "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", 116 | "engines": { 117 | "node": ">=10" 118 | }, 119 | "funding": { 120 | "url": "https://github.com/sponsors/sindresorhus" 121 | } 122 | }, 123 | "node_modules/defer-to-connect": { 124 | "version": "2.0.1", 125 | "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", 126 | "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", 127 | "engines": { 128 | "node": ">=10" 129 | } 130 | }, 131 | "node_modules/form-data-encoder": { 132 | "version": "4.0.2", 133 | "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.0.2.tgz", 134 | "integrity": "sha512-KQVhvhK8ZkWzxKxOr56CPulAhH3dobtuQ4+hNQ+HekH/Wp5gSOafqRAeTphQUJAIk0GBvHZgJ2ZGRWd5kphMuw==", 135 | "engines": { 136 | "node": ">= 18" 137 | } 138 | }, 139 | "node_modules/get-stream": { 140 | "version": "8.0.1", 141 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", 142 | "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", 143 | "engines": { 144 | "node": ">=16" 145 | }, 146 | "funding": { 147 | "url": "https://github.com/sponsors/sindresorhus" 148 | } 149 | }, 150 | "node_modules/got": { 151 | "version": "14.2.1", 152 | "resolved": "https://registry.npmjs.org/got/-/got-14.2.1.tgz", 153 | "integrity": "sha512-KOaPMremmsvx6l9BLC04LYE6ZFW4x7e4HkTe3LwBmtuYYQwpeS4XKqzhubTIkaQ1Nr+eXxeori0zuwupXMovBQ==", 154 | "dependencies": { 155 | "@sindresorhus/is": "^6.1.0", 156 | "@szmarczak/http-timer": "^5.0.1", 157 | "cacheable-lookup": "^7.0.0", 158 | "cacheable-request": "^10.2.14", 159 | "decompress-response": "^6.0.0", 160 | "form-data-encoder": "^4.0.2", 161 | "get-stream": "^8.0.1", 162 | "http2-wrapper": "^2.2.1", 163 | "lowercase-keys": "^3.0.0", 164 | "p-cancelable": "^4.0.1", 165 | "responselike": "^3.0.0" 166 | }, 167 | "engines": { 168 | "node": ">=20" 169 | }, 170 | "funding": { 171 | "url": "https://github.com/sindresorhus/got?sponsor=1" 172 | } 173 | }, 174 | "node_modules/http-cache-semantics": { 175 | "version": "4.1.1", 176 | "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", 177 | "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" 178 | }, 179 | "node_modules/http2-wrapper": { 180 | "version": "2.2.1", 181 | "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", 182 | "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", 183 | "dependencies": { 184 | "quick-lru": "^5.1.1", 185 | "resolve-alpn": "^1.2.0" 186 | }, 187 | "engines": { 188 | "node": ">=10.19.0" 189 | } 190 | }, 191 | "node_modules/json-buffer": { 192 | "version": "3.0.1", 193 | "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", 194 | "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" 195 | }, 196 | "node_modules/keyv": { 197 | "version": "4.5.4", 198 | "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", 199 | "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", 200 | "dependencies": { 201 | "json-buffer": "3.0.1" 202 | } 203 | }, 204 | "node_modules/lowercase-keys": { 205 | "version": "3.0.0", 206 | "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", 207 | "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", 208 | "engines": { 209 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 210 | }, 211 | "funding": { 212 | "url": "https://github.com/sponsors/sindresorhus" 213 | } 214 | }, 215 | "node_modules/mimic-response": { 216 | "version": "4.0.0", 217 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", 218 | "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", 219 | "engines": { 220 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 221 | }, 222 | "funding": { 223 | "url": "https://github.com/sponsors/sindresorhus" 224 | } 225 | }, 226 | "node_modules/normalize-url": { 227 | "version": "8.0.1", 228 | "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", 229 | "integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==", 230 | "engines": { 231 | "node": ">=14.16" 232 | }, 233 | "funding": { 234 | "url": "https://github.com/sponsors/sindresorhus" 235 | } 236 | }, 237 | "node_modules/p-cancelable": { 238 | "version": "4.0.1", 239 | "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz", 240 | "integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==", 241 | "engines": { 242 | "node": ">=14.16" 243 | } 244 | }, 245 | "node_modules/quick-lru": { 246 | "version": "5.1.1", 247 | "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", 248 | "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", 249 | "engines": { 250 | "node": ">=10" 251 | }, 252 | "funding": { 253 | "url": "https://github.com/sponsors/sindresorhus" 254 | } 255 | }, 256 | "node_modules/resolve-alpn": { 257 | "version": "1.2.1", 258 | "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", 259 | "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" 260 | }, 261 | "node_modules/responselike": { 262 | "version": "3.0.0", 263 | "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", 264 | "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", 265 | "dependencies": { 266 | "lowercase-keys": "^3.0.0" 267 | }, 268 | "engines": { 269 | "node": ">=14.16" 270 | }, 271 | "funding": { 272 | "url": "https://github.com/sponsors/sindresorhus" 273 | } 274 | } 275 | } 276 | } 277 | --------------------------------------------------------------------------------