├── .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 |
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 |
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 |
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 | 
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 | 
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 | [](https://badge.fury.io/js/camunda-external-task-client-js)
4 | 
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 |
--------------------------------------------------------------------------------