@camunda8/sdk
Version:
[](https://www.npmjs.com/package/@camunda8/sdk)
1,180 lines (1,179 loc) • 50.8 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CamundaRestClient = void 0;
const node_fs_1 = __importDefault(require("node:fs"));
const debug_1 = require("debug");
const form_data_1 = __importDefault(require("form-data"));
const got_1 = __importDefault(require("got"));
const lossless_json_1 = require("lossless-json");
const p_cancelable_1 = __importDefault(require("p-cancelable"));
const lib_1 = require("../../lib");
const types_1 = require("../../zeebe/types");
const C8Dto_1 = require("./C8Dto");
const C8Logger_1 = require("./C8Logger");
const CamundaJobWorker_1 = require("./CamundaJobWorker");
const OriginTracing_1 = require("./OriginTracing");
const RestApiJobClassFactory_1 = require("./RestApiJobClassFactory");
const RestApiProcessInstanceClassFactory_1 = require("./RestApiProcessInstanceClassFactory");
const TrackedGot_1 = require("./TrackedGot");
const trace = (0, debug_1.debug)('camunda:orchestration-rest');
// Storage moved to TrackedGot.ts and imported above
const CAMUNDA_REST_API_VERSION = 'v2';
class DefaultLosslessDto extends lib_1.LosslessDto {
}
// createTrackedGot imported from TrackedGot.ts
/**
* The client for the unified Camunda 8 Orchestration Cluster REST API.
*
* Logging: to enable debug tracing during development, you can set `DEBUG=camunda:orchestration-rest`.
*
* For production, you can pass in an logger compatible with {@link Logger} to the constructor as `logger`.
*
* `CAMUNDA_LOG_LEVEL` in the environment or the constructor options can be used to set the log level to one of 'error', 'warn', 'info', 'http', 'verbose', 'debug', or 'silly'.
*
* @since 8.6.0
*/
let CamundaRestClient = class CamundaRestClient {
/**
* All constructor parameters for configuration are optional. If no configuration is provided, the SDK will use environment variables to configure itself.
*/
constructor(options) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.workers = [];
/**
* Helper method to add the default job methods to a job
* @param job The job to add the methods to
* @returns The job with the added methods
*/
this.addJobMethods = (job) => {
return {
...job,
cancelWorkflow: async () => {
await this.cancelProcessInstance({
processInstanceKey: job.processInstanceKey,
});
return types_1.JOB_ACTION_ACKNOWLEDGEMENT;
},
complete: (variables = {}) => this.completeJob({
jobKey: job.jobKey,
variables,
}),
error: (error) => this.errorJob({
...error,
jobKey: job.jobKey,
}),
fail: (failJobRequest) => this.failJob({
jobKey: job.jobKey,
errorMessage: failJobRequest.errorMessage,
retries: failJobRequest.retries ?? job.retries - 1,
retryBackOff: failJobRequest.retryBackOff ?? 0,
variables: failJobRequest.variables,
}),
/* This has an effect in a Job Worker, decrementing the currently active job count */
forward: () => types_1.JOB_ACTION_ACKNOWLEDGEMENT,
modifyJobTimeout: ({ newTimeoutMs }) => this.updateJob({ jobKey: job.jobKey, timeout: newTimeoutMs }),
};
};
const config = lib_1.CamundaEnvironmentConfigurator.mergeConfigWithEnvironment(options?.config ?? {});
this.config = config;
this.log = (0, C8Logger_1.getLogger)(config);
this.log.debug(`Using REST API version ${CAMUNDA_REST_API_VERSION}`);
trace('options.config', options?.config);
trace('config', config);
this.oAuthProvider =
options?.oAuthProvider ??
(0, lib_1.constructOAuthProvider)(config, {
explicitFromConstructor: Object.prototype.hasOwnProperty.call(options?.config ?? {}, 'CAMUNDA_AUTH_STRATEGY'),
});
this.userAgentString = (0, lib_1.createUserAgentString)(config);
this.tenantId = config.CAMUNDA_TENANT_ID;
const baseUrl = (0, lib_1.RequireConfiguration)(config.ZEEBE_REST_ADDRESS, 'ZEEBE_REST_ADDRESS');
this.prefixUrl = `${baseUrl}/${CAMUNDA_REST_API_VERSION}`;
this.rest = (0, lib_1.GetCustomCertificateBuffer)(config).then((certificateAuthority) => (0, TrackedGot_1.createTrackedGot)(got_1.default.extend({
prefixUrl: this.prefixUrl,
retry: lib_1.GotRetryConfig,
https: {
certificateAuthority,
},
handlers: [],
hooks: {
beforeError: [(0, lib_1.gotBeforeErrorHook)(config)],
beforeRequest: [
(options) => {
const body = options.body;
const path = options.url.href;
const method = options.method;
trace(`${method} ${path}`);
trace(body);
this.log.debug(`${method} ${path}`);
this.log.trace(body?.toString());
const traceData = options.context?.__stackTrace;
if (traceData) {
this.log.debug(`\n${'='.repeat(70)}`);
this.log.debug(`HTTP Request [${traceData.requestId}]`);
this.log.debug(`${'='.repeat(70)}`);
this.log.debug(`URL: ${options.method} ${options.url.href}`);
this.log.debug(`\nCall Chain (${traceData.stacks.length} frames):`);
traceData.stacks.forEach((entry, i) => {
this.log.debug(` ${i + 1}. ${entry.location}`);
});
this.log.debug(`${'='.repeat(70)}\n`);
}
},
...(config.middleware ?? []),
],
},
})));
}
async getHeaders() {
const authorization = await this.oAuthProvider.getHeaders('ZEEBE');
const headers = {
'content-type': 'application/json',
...authorization,
'user-agent': this.userAgentString,
accept: '*/*',
};
const auth = headers.authorization ? headers.authorization : headers.cookie;
const safeHeaders = {
...headers,
authorization: auth
? auth.substring(0, 15) + (auth.length > 8)
? '...'
: ''
: '',
};
trace('headers', safeHeaders);
return headers;
}
/**
* Manage the permissions assigned to authorization.
*
* Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/patch-authorization/
*
* @since 8.6.0
*/
async modifyAuthorization(req) {
const headers = await this.getHeaders();
const { ownerKey, ...request } = req;
return this.rest.then((rest) => rest
.patch(`authorizations/${ownerKey}`, {
headers,
body: (0, lossless_json_1.stringify)(request),
})
.json());
}
/**
* Broadcast a signal.
*
* Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/broadcast-signal/
*
* @since 8.6.0
*/
async broadcastSignal(req) {
const headers = await this.getHeaders();
const request = this.addDefaultTenantId(req);
return this.rest.then((rest) => rest
.post(`signals/broadcast`, {
headers,
body: (0, lossless_json_1.stringify)(request),
parseJson: (text) => (0, lib_1.losslessParse)(text, C8Dto_1.BroadcastSignalResponse),
})
.json());
}
/* Get the topology of the Zeebe cluster. */
async getTopology() {
const headers = await this.getHeaders();
return this.rest.then((rest) => rest.get('topology', { headers }).json());
}
/**
* Complete a user task with the given key. The method either completes the task or throws 400, 404, or 409.
*
* Documentation: https://docs.camunda.io/docs/apis-tools/zeebe-api-rest/specifications/complete-a-user-task/
*
* @since 8.6.0
*/
async completeUserTask({ userTaskKey, variables = {}, action = 'complete', }) {
const headers = await this.getHeaders();
return this.rest.then((rest) => rest
.post(`user-tasks/${userTaskKey}/completion`, {
body: (0, lib_1.losslessStringify)({
variables,
action,
}),
headers,
})
.json());
}
/**
* Stop all workers that were created by this client.
*/
stopWorkers() {
this.workers.forEach((worker) => worker.stop());
}
/**
* Start all workers that were created by this client.
*/
startWorkers() {
this.workers.forEach((worker) => worker.start());
}
/**
* Assign a user task with the given key to the given assignee.
*
* Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/assign-user-task/
*
* @since 8.6.0
* @deprecated use `assignUserTask`
*/
async assignTask({ userTaskKey, assignee, allowOverride = true, action = 'assign', }) {
return this.assignUserTask({ userTaskKey, assignee, allowOverride, action });
}
/**
* Assign a user task with the given key to the given assignee.
*
* Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/assign-user-task/
*
* @since 8.6.0
*/
async assignUserTask({ userTaskKey, assignee, allowOverride = true, action = 'assign', }) {
const headers = await this.getHeaders();
const req = {
allowOverride,
action,
assignee,
};
return this.rest.then((rest) => rest
.post(`user-tasks/${userTaskKey}/assignment`, {
body: (0, lib_1.losslessStringify)(req),
headers,
})
.json());
}
/**
* Update a user task with the given key.
*
* Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/update-user-task/
*
* @since 8.6.0
* @deprecated use `updateUserTask`
*/
async updateTask({ userTaskKey, changeset, }) {
return this.updateUserTask({ userTaskKey, changeset });
}
/**
* Update a user task with the given key.
*
* Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/update-user-task/
*
* @since 8.6.0
*/
async updateUserTask({ userTaskKey, changeset, }) {
const headers = await this.getHeaders();
return this.rest.then((rest) => rest
.patch(`user-tasks/${userTaskKey}/update`, {
body: (0, lib_1.losslessStringify)(changeset),
headers,
})
.json());
}
/**
* Remove the assignee of a task with the given key.
*
* Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/unassign-user-task/
*
* @since 8.6.0
* @deprecated use `unassignUserTask`
*/
async unassignTask({ userTaskKey, }) {
return this.unassignUserTask({ userTaskKey });
}
/**
* Remove the assignee of a task with the given key.
*
* Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/unassign-user-task/
*
* @since 8.6.0
*/
async unassignUserTask({ userTaskKey, }) {
const headers = await this.getHeaders();
return this.rest.then((rest) => rest.delete(`user-tasks/${userTaskKey}/assignee`, { headers }).json());
}
/**
* Search for user tasks based on given criteria.
*
* Documentation: https://docs.camunda.io/docs/8.7/apis-tools/camunda-api-rest/specifications/find-user-tasks/
*
* @since 8.8.0
*/
async searchUserTasks(request) {
const headers = await this.getHeaders();
const page = request.page ?? {
from: 0,
limit: 100,
};
const sort = request.sort ?? [{ field: 'creationDate', order: 'asc' }];
const response = await this.rest.then((rest) => rest
.post(`user-tasks/search`, {
headers,
body: (0, lib_1.losslessStringify)({ ...request, page, sort }),
})
.json());
/**
* The 8.6 and 8.7 API have different key names for the userTaskKey. This code block normalizes the key names.
*/
const normalizedResponse = {
...response,
items: response.items.map((item) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (!item.userTaskKey && item.key) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
item.userTaskKey = item.key;
}
return item;
}),
};
return normalizedResponse;
}
/**
* Get the user task by the user task key.
*
* Documentation: https://docs.camunda.io/docs/next/apis-tools/camunda-api-rest/specifications/get-user-task/
*
* @since 8.8.0
*/
async getUserTask(userTaskKey) {
const headers = await this.getHeaders();
return this.rest.then((rest) => rest.get(`user-tasks/${userTaskKey}`, { headers }).json());
}
/**
*
* Search for user task variables based on given criteria.
*
* Documentation: https://docs.camunda.io/docs/next/apis-tools/camunda-api-rest/specifications/find-user-task-variables/
*
* @since 8.8.0
*/
async searchUserTaskVariables(request) {
const { userTaskKey, ...req } = request;
const headers = await this.getHeaders();
return this.rest.then((rest) => rest
.post(`user-tasks/${userTaskKey}/variables/search`, {
headers,
body: (0, lib_1.losslessStringify)(req),
})
.json());
}
/**
* Create a user.
*
* Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/create-user/
*
* @since 8.6.0
*/
async createUser(newUserInfo) {
const headers = await this.getHeaders();
return this.rest.then((rest) => rest
.post(`users`, {
body: (0, lib_1.losslessStringify)(newUserInfo),
headers,
})
.json());
}
/**
* Search users for tenant.
*
* Documentation: https://docs.camunda.io/docs/next/apis-tools/camunda-api-rest/specifications/find-users/
*
* @since 8.8.0
*/
async searchUsers(request) {
const headers = await this.getHeaders();
return this.rest.then((rest) => rest
.post(`users/search`, {
body: (0, lib_1.losslessStringify)(request),
headers,
})
.json());
}
/**
* Search users for tenant.
*
* Documentation: https://docs.camunda.io/docs/next/apis-tools/camunda-api-rest/specifications/search-users-for-tenant/
*
* @since 8.8.0
*/
async searchUsersForTenant(tenantId, request) {
const headers = await this.getHeaders();
return this.rest.then((rest) => rest
.post(`tenants/${tenantId}/users/search`, {
body: (0, lib_1.losslessStringify)(request),
headers,
})
.json());
}
/**
* Publish a Message and correlates it to a subscription. If correlation is successful it will return the first process instance key the message correlated with.
*
* Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/correlate-a-message/
*
* @since 8.6.0
*/
async correlateMessage(message) {
const headers = await this.getHeaders();
const req = this.addDefaultTenantId(message);
const body = (0, lib_1.losslessStringify)(req);
return this.rest.then((rest) => rest
.post(`messages/correlation`, {
body,
headers,
parseJson: (text) => (0, lib_1.losslessParse)(text, C8Dto_1.CorrelateMessageResponse),
})
.json());
}
/**
* Publish a single message. Messages are published to specific partitions computed from their correlation keys. This method does not wait for a correlation result. Use `correlateMessage` for such use cases.
*
* Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/publish-a-message/
*
* @since 8.6.0
*/
async publishMessage(publishMessageRequest) {
const headers = await this.getHeaders();
const req = this.addDefaultTenantId(publishMessageRequest);
const body = (0, lib_1.losslessStringify)(req);
return this.rest.then((rest) => rest
.post(`messages/publication`, {
headers,
body,
parseJson: (text) => (0, lib_1.losslessParse)(text, C8Dto_1.PublishMessageResponse),
})
.json());
}
/**
* Obtains the status of the current Camunda license.
*
* Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/get-status-of-camunda-license/
*
* @since 8.6.0
*/
async getLicenseStatus() {
return this.rest.then((rest) => rest.get(`license`).json());
}
/**
* Create a new polling Job Worker.
* You can pass in an optional winston.Logger instance as `logger`. This enables you to have distinct logging levels for different workers.
*
* Polling: The worker polls periodically. If no jobs are available, the poll stays open for 10 seconds.
* If no jobs become available in that time, the poll is closed, and the worker polls again.
* When jobs are available, they are returned, and the worker polls again for more jobs as soon as it has capacity for more jobs.
*
* @since 8.6.0
*/
createJobWorker(config) {
const worker = new CamundaJobWorker_1.CamundaJobWorker(config, this);
this.workers.push(worker);
return worker;
}
/**
* Iterate through all known partitions and activate jobs up to the requested maximum. This method returns a PCancelable promise that resolves to an array of activated jobs.
* The promise will resolve at the request timeout, or as soon as jobs are available.
*
* The parameter `inputVariablesDto` is a Dto to decode the job payload. The `customHeadersDto` parameter is a Dto to decode the custom headers.
* Pass in a Dto class that extends LosslessDto to provide both type information in your code,
* and safe interoperability with applications that use the `int64` type in variables.
*
* @since 8.6.0
*/
activateJobs(request) {
const { inputVariableDto = lib_1.LosslessDto, customHeadersDto = lib_1.LosslessDto, tenantIds = this.tenantId ? [this.tenantId] : undefined, ...req } = request;
// The ActivateJobs endpoint can take multiple tenantIds, and activate jobs for multiple tenants at once.
// Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/activate-jobs/
const body = {
...req,
tenantIds,
};
const jobDto = (0, RestApiJobClassFactory_1.createSpecializedRestApiJobClass)(inputVariableDto, customHeadersDto);
const gotRequest = this.callApiEndpoint({
urlPath: 'jobs/activation',
method: 'POST',
body,
parseJson: (text) => (0, lib_1.losslessParse)(text, jobDto, 'jobs'),
});
// Make the call cancelable
// See: https://github.com/camunda/camunda-8-js-sdk/issues/424
return new p_cancelable_1.default((resolve, reject, onCancel) => {
onCancel.shouldReject = false;
onCancel(() => !gotRequest.isCanceled &&
gotRequest.cancel(`Cancelling activateJobs request`));
gotRequest
.then((activatedJobs) => activatedJobs.map(this.addJobMethods))
.then(resolve)
.catch((error) => {
// Normalise and wrap backpressure errors in a specific error type for clarity for users
// See: https://github.com/camunda/camunda/issues/25806#issuecomment-3459961630
if (error instanceof lib_1.HTTPError) {
const RESOURCE_EXHAUSTED = 'RESOURCE_EXHAUSTED';
const isBackpressureError = (error.statusCode === 429 &&
error.title === RESOURCE_EXHAUSTED) || // 8.6.0-8.6.29 / 8.7.0-8.7.16
(error.statusCode === 503 &&
error.title === RESOURCE_EXHAUSTED) || // 8.8.3+
(error.statusCode === 500 &&
error.detail?.includes(RESOURCE_EXHAUSTED)); // 8.8.0-8.8.2 for job activation only
if (!isBackpressureError) {
reject(error);
}
if (error.statusCode === 500) {
error.code = '503';
error.statusCode = 503;
}
error.message = `Server is experiencing backpressure and cannot accept job activations at this time: ${error.originalMessage}\n`;
}
reject(error);
});
});
}
/**
* Fails a job using the provided job key. This method sends a POST request to the endpoint '/jobs/{jobKey}/fail' with the failure reason and other details specified in the failJobRequest object.
*
* Documentation: https://docs.camunda.io/docs/next/apis-tools/camunda-api-rest/specifications/fail-job/
*
* @since 8.6.0
*/
async failJob(failJobRequest) {
const { jobKey, ...body } = failJobRequest;
const headers = await this.getHeaders();
return this.rest.then((rest) => rest
.post(`jobs/${jobKey}/failure`, {
body: (0, lib_1.losslessStringify)(body),
headers,
})
.then(() => types_1.JOB_ACTION_ACKNOWLEDGEMENT));
}
/**
* Report a business error (i.e. non-technical) that occurs while processing a job.
*
* Documentation: https://docs.camunda.io/docs/next/apis-tools/camunda-api-rest/specifications/report-error-for-job/
*
* @since 8.6.0
*/
async errorJob(errorJobRequest) {
const { jobKey, ...request } = errorJobRequest;
const headers = await this.getHeaders();
return this.rest.then((rest) => rest
.post(`jobs/${jobKey}/error`, {
body: (0, lib_1.losslessStringify)(request),
headers,
parseJson: (text) => (0, lib_1.losslessParse)(text),
})
.then(() => types_1.JOB_ACTION_ACKNOWLEDGEMENT));
}
/**
* Complete a job with the given payload, which allows completing the associated service task.
*
* Documentation: https://docs.camunda.io/docs/next/apis-tools/camunda-api-rest/specifications/complete-job/
*
* @since 8.6.0
*/
async completeJob(completeJobRequest) {
const { jobKey } = completeJobRequest;
const headers = await this.getHeaders();
const req = { variables: completeJobRequest.variables };
return this.rest.then((rest) => rest
.post(`jobs/${jobKey}/completion`, {
body: (0, lib_1.losslessStringify)(req),
headers,
})
.then(() => types_1.JOB_ACTION_ACKNOWLEDGEMENT));
}
/**
* Update a job with the given key.
*
* Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/update-a-job/
*
* @since 8.6.0
*/
async updateJob(jobChangeset) {
const { jobKey, ...changeset } = jobChangeset;
return this.callApiEndpoint({
urlPath: `jobs/${jobKey}`,
method: 'PATCH',
body: { changeset },
json: false,
});
}
/**
* Marks the incident as resolved; most likely a call to Update job will be necessary to reset the job's retries, followed by this call.
*
* Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/resolve-incident/
*
* @since 8.6.0
*/
async resolveIncident(incidentKey) {
const headers = await this.getHeaders();
return this.rest.then((rest) => rest.post(`incidents/${incidentKey}/resolution`, {
headers,
}));
}
async createProcessInstance(request) {
const outputVariablesDto = request.outputVariablesDto ?? DefaultLosslessDto;
const CreateProcessInstanceResponseWithVariablesDto = (0, RestApiProcessInstanceClassFactory_1.createSpecializedCreateProcessInstanceResponseClass)(outputVariablesDto);
return this.callApiEndpoint({
urlPath: `process-instances`,
method: 'POST',
body: this.addDefaultTenantId(request),
parseJson: (text) => (0, lib_1.losslessParse)(text, CreateProcessInstanceResponseWithVariablesDto),
});
}
async createProcessInstanceWithResult(request) {
/**
* We override the type system to make `awaitCompletion` hidden from end-users. This has been done because supporting the permutations of
* creating a process with/without awaiting the result and with/without an outputVariableDto in a single method is complex. I could not get all
* the cases to work with intellisense for the end-user using either generics or with signature overloads.
*
* To address this, createProcessInstance has all the functionality, but hides the `awaitCompletion` attribute from the signature. This method
* is a wrapper around createProcessInstance that sets `awaitCompletion` to true, and explicitly informs the type system via signature overloads.
*
* This is not ideal, but it is the best solution I could come up with.
*/
return this.createProcessInstance({
...request,
awaitCompletion: true,
outputVariablesDto: request.outputVariablesDto,
});
}
/**
* Cancel an active process instance
*/
async cancelProcessInstance({ processInstanceKey, operationReference, }) {
return this.callApiEndpoint({
urlPath: `process-instances/${processInstanceKey}/cancellation`,
method: 'POST',
body: { operationReference },
});
}
/**
* Migrates a process instance to a new process definition.
* This request can contain multiple mapping instructions to define mapping between the active process instance's elements and target process definition elements.
* Use this to upgrade a process instance to a new version of a process or to a different process definition, e.g. to keep your running instances up-to-date with the latest process improvements.
*
* Documentation: https://docs.camunda.io/docs/next/apis-tools/camunda-api-rest/specifications/migrate-process-instance/
*
* @since 8.6.0
*/
async migrateProcessInstance(req) {
const { processInstanceKey, ...request } = req;
this.log.debug(`Migrating process instance ${processInstanceKey}`, {
component: 'C8RestClient',
});
return this.callApiEndpoint({
urlPath: `process-instances/${processInstanceKey}/migration`,
method: 'POST',
body: request,
});
}
/**
* Query process instances
*
* Documentation: https://docs.camunda.io/docs/8.7/apis-tools/camunda-api-rest/specifications/query-process-instances-alpha/
*
* @since 8.8.0
*/
async searchProcessInstances(request) {
return this.callApiEndpoint({
urlPath: `process-instances/search`,
method: 'POST',
body: request,
});
}
/**
* Deploy resources to the broker.
* @param resources - An array of binary data strings representing the resources to deploy.
* @param tenantId - Optional tenant ID to deploy the resources to. If not provided, the default tenant ID is used.
*
* Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/deploy-resources/
*
* @since 8.6.0
*/
async deployResources(resources, tenantId) {
const headers = await this.getHeaders();
const formData = new form_data_1.default();
resources.forEach((resource) => {
formData.append(`resources`, resource.content, {
filename: resource.name,
});
});
if (tenantId || this.tenantId) {
formData.append('tenantId', tenantId ?? this.tenantId);
}
this.log.debug(`Deploying ${resources.length} resources`);
const res = await this.rest.then((rest) => rest
.post('deployments', {
body: formData,
headers: {
...headers,
...formData.getHeaders(),
Accept: 'application/json',
},
parseJson: (text) => (0, lossless_json_1.parse)(text), // we parse the response with LosslessNumbers, with no Dto
})
.json());
/**
* Now we need to examine the response and parse the deployments to lossless Dtos
* We dynamically construct the response object for the caller, by examining the lossless response
* and re-parsing each of the deployments with the correct Dto.
*/
const deploymentResponse = new C8Dto_1.DeployResourceResponse();
deploymentResponse.deploymentKey = res.deploymentKey.toString();
deploymentResponse.tenantId = res.tenantId;
deploymentResponse.deployments = [];
deploymentResponse.processes = [];
deploymentResponse.decisions = [];
deploymentResponse.forms = [];
deploymentResponse.decisionRequirements = [];
/**
* Type-guard assertions to correctly type the deployments. The API returns an array with mixed types.
*/
const isProcessDeployment = (deployment) => !!deployment.processDefinition;
const isDecisionDeployment = (deployment) => !!deployment.decisionDefinition;
const isDecisionRequirementsDeployment = (deployment) => !!deployment.decisionRequirements;
const isFormDeployment = (deployment) => !!deployment.form;
/**
* Here we examine each of the deployments returned from the API, and create a correctly typed
* object for each one. We also populate subkeys per type. This allows SDK users to work with
* types known ahead of time.
*/
res.deployments.forEach((deployment) => {
if (isProcessDeployment(deployment)) {
const processDeployment = (0, lib_1.losslessParse)((0, lossless_json_1.stringify)(deployment.processDefinition), C8Dto_1.ProcessDeployment);
deploymentResponse.deployments.push({
processDefinition: processDeployment,
});
deploymentResponse.processes.push(processDeployment);
}
if (isDecisionDeployment(deployment)) {
const decisionDeployment = (0, lib_1.losslessParse)((0, lossless_json_1.stringify)(deployment.decisionDefinition), C8Dto_1.DecisionDeployment);
deploymentResponse.deployments.push({
decisionDefinition: decisionDeployment,
});
deploymentResponse.decisions.push(decisionDeployment);
}
if (isDecisionRequirementsDeployment(deployment)) {
const decisionRequirementsDeployment = (0, lib_1.losslessParse)((0, lossless_json_1.stringify)(deployment.decisionRequirements), C8Dto_1.DecisionRequirementsDeployment);
deploymentResponse.deployments.push({
decisionRequirements: decisionRequirementsDeployment,
});
deploymentResponse.decisionRequirements.push(decisionRequirementsDeployment);
}
if (isFormDeployment(deployment)) {
const formDeployment = (0, lib_1.losslessParse)((0, lossless_json_1.stringify)(deployment.form), C8Dto_1.FormDeployment);
deploymentResponse.deployments.push({ form: formDeployment });
deploymentResponse.forms.push(formDeployment);
}
});
return deploymentResponse;
}
/**
* Deploy resources to Camunda 8 from files
* @param files an array of file paths
*
* @since 8.6.0
*/
async deployResourcesFromFiles(files, { tenantId } = {}) {
const resources = [];
for (const file of files) {
resources.push({
content: node_fs_1.default.readFileSync(file, { encoding: 'binary' }),
name: file,
});
}
return this.deployResources(resources, tenantId ?? this.tenantId);
}
/**
* Deletes a deployed resource. This can be a process definition, decision requirements definition, or form definition deployed using the deploy resources endpoint. Specify the resource you want to delete in the resourceKey parameter.
*
* Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/delete-resource/
*
* @since 8.6.0
*/
async deleteResource(req) {
const headers = await this.getHeaders();
const { resourceKey, operationReference } = req;
return this.rest.then((rest) => rest.post(`resources/${resourceKey}/deletion`, {
headers,
body: (0, lossless_json_1.stringify)({ operationReference }),
}));
}
/**
* Set a precise, static time for the Zeebe engine's internal clock.
* When the clock is pinned, it remains at the specified time and does not advance.
* To change the time, the clock must be pinned again with a new timestamp, or reset.
*
* Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/pin-internal-clock/
*
* @since 8.6.0
*/
async pinInternalClock(epochMs) {
return this.callApiEndpoint({
urlPath: `clock`,
method: 'PUT',
body: { timestamp: epochMs },
});
}
/**
* Resets the Zeebe engine's internal clock to the current system time, enabling it to tick in real-time.
* This operation is useful for returning the clock to normal behavior after it has been pinned to a specific time.
*
* Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/reset-internal-clock/
*
* @since 8.6.0
*/
async resetClock() {
return this.callApiEndpoint({ urlPath: `clock/reset`, method: 'POST' });
}
/**
* Updates all the variables of a particular scope (for example, process instance, flow element instance) with the given variable data.
* Specify the element instance in the elementInstanceKey parameter.
*
* Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/update-element-instance-variables/
*
* @since 8.6.0
*/
async updateElementInstanceVariables(req) {
const { elementInstanceKey, ...body } = req;
return this.callApiEndpoint({
urlPath: `element-instances/${elementInstanceKey}/variables`,
method: 'PUT',
body,
});
}
getConfig() {
return this.config;
}
/**
* Search for process and local variables based on given criteria.
*
* Documentation: https://docs.camunda.io/docs/next/apis-tools/camunda-api-rest/specifications/find-variables/
* @since 8.8.0
*/
async searchVariables(req) {
return this.callApiEndpoint({
urlPath: `variables/search`,
method: 'POST',
body: req,
});
}
/**
* Download a document from the Camunda 8 cluster.
*
* Note that this is currently supported for document stores of type: AWS, GCP, in-memory, local
* Documentation: https://docs.camunda.io/docs/8.7/apis-tools/camunda-api-rest/specifications/get-document/
*
* @since 8.7.0
*/
async downloadDocument(request) {
const headers = await this.getHeaders();
return this.rest.then((rest) => rest
.get(`documents/${request.documentId}`, {
headers: {
...headers, // we need the headers to be passed in
accept: '*/*',
},
searchParams: {
contentHash: request.contentHash,
storeId: request.storeId,
},
})
.buffer());
}
/**
* Upload a document to the Camunda 8 cluster.
* Note that this is currently supported for document stores of type: AWS, GCP, in-memory, local
*
* Documentation: https://docs.camunda.io/docs/8.7/apis-tools/camunda-api-rest/specifications/create-document/
* @since 8.7.0
*/
async uploadDocument(request) {
const headers = await this.getHeaders();
const formData = new form_data_1.default();
const options = request.metadata?.contentType || request.metadata?.fileName
? {
contentType: request.metadata?.contentType,
filename: request.metadata?.fileName,
}
: {};
formData.append('file', request.file, options);
// Add other form fields
if (request.metadata) {
formData.append('metadata', JSON.stringify(request.metadata), {
contentType: 'application/json',
});
}
return this.rest.then((rest) => rest
.post('documents', {
searchParams: {
storeId: request.storeId,
documentId: request.documentId,
},
headers: {
...headers,
...formData.getHeaders(),
accept: 'application/json',
},
body: formData,
parseJson: (text) => (0, lib_1.losslessParse)(text, C8Dto_1.UploadDocumentResponse),
})
.json());
}
/**
* Delete a document from the Camunda 8 cluster.
*
* Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/delete-document/
*
* @since 8.7.0
*/
async deleteDocument({ documentId, storeId, }) {
return this.callApiEndpoint({
method: 'DELETE',
urlPath: `documents/${documentId}`,
queryParams: {
storeId: storeId ? storeId : undefined,
},
});
}
/**
*
* Upload multiple documents to the Camunda 8 cluster.
* The caller must provide a file name for each document, which will be used in case of a multi-status response to identify which documents failed to upload.
* The file name can be provided in the Content-Disposition header of the file part or in the fileName field of the metadata part.
* If both are provided, the fileName field takes precedence.
*
* In case of a multi-status response, the response body will contain a list of DocumentBatchProblemDetail objects,
* each of which contains the file name of the document that failed to upload and the reason for the failure.
* The client can choose to retry the whole batch or individual documents based on the response.
*
* Note that this is currently supported for document stores of type: AWS, GCP, in-memory (non-production), local (non-production)
*
* The filename is inferred from the filepath. If you are not reading the files from disk, you need to set the path, like this:
*
* ```typescript
* // In-memory conversion: create Readable streams exposing a path so form-data infers filename
const streams: ReadStream[] = uploadedFiles.map((file) => {
const stream = new Readable({
read() {
this.push(file.buffer);
this.push(null);
}
});
// Provide minimal fs.ReadStream-like fields used by form-data for filename inference.
(stream as any).path = file.originalname;
(stream as any).close = () => stream.destroy();
return stream as unknown as ReadStream;
});
```
*
* Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/create-documents/
* @since 8.7.0
*/
async uploadDocuments(request) {
const headers = await this.getHeaders();
const formData = new form_data_1.default();
for (const file of request.files) {
formData.append('files', file);
}
return this.rest.then((rest) => rest
.post('documents/batch', {
searchParams: {
storeId: request.storeId ? request.storeId : undefined,
},
headers: {
...headers,
...formData.getHeaders(),
accept: 'application/json',
},
body: formData,
parseJson: (text) => (0, lib_1.losslessParse)(text, C8Dto_1.UploadDocumentsResponse),
})
.json());
}
/**
* Create document link
*
* Create a link to a document in the Camunda 8 cluster.
* Note that this is currently supported for document stores of type: AWS, GCP
*
* Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/create-document-link/
* @since 8.7.0
*/
async createDocumentLink(request) {
return this.callApiEndpoint({
method: 'POST',
urlPath: `documents/${request.documentId}/link`,
body: {
timeToLive: request.timeToLive,
},
queryParams: {
storeId: request.storeId ? request.storeId : undefined,
contentHash: request.contentHash ? request.contentHash : undefined,
},
});
}
/**
* Modify process instance
*
* Modifies a running process instance. This request can contain multiple instructions to activate an element of the process or to terminate an active instance of an element.
* Use this to repair a process instance that is stuck on an element or took an unintended path. For example, because an external system is not available or doesn't respond as expected.
*
* Documentation https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/modify-process-instance/
* @since 8.6.0
*/
async modifyProcessInstance(request) {
const { processInstanceKey, ...req } = request;
return this.callApiEndpoint({
method: 'POST',
urlPath: `process-instances/${processInstanceKey}/modification`,
body: req,
});
}
/**
* Evaluate decision
*
* Evaluates a decision. You specify the decision to evaluate either by using its unique key (as returned by DeployResource), or using the decision ID.
* When using the decision ID, the latest deployed version of the decision is used.
*
* Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/evaluate-decision/
* @since 8.6.0
*/
async evaluateDecision(request) {
return this.callApiEndpoint({
method: 'POST',
urlPath: `decision-definitions/evaluation`,
body: this.addDefaultTenantId(request),
});
}
/**
* @description Get the variable by the variable key. Documentation: https://docs.camunda.io/docs/next/apis-tools/camunda-api-rest/specifications/get-variable/
* @since 8.8.0
*/
async getVariable(req) {
return this.callApiEndpoint({
method: 'GET',
urlPath: `variables/${req.variableKey}`,
}).then((response) => {
// We need to parse the response with LosslessNumbers, as the API returns numbers as strings
return {
...response,
value: JSON.parse(response.value),
};
});
}
/**
* @description Returns process definition as XML.
* @param processDefinitionKey The assigned key of the process definition, which acts as a unique identifier for this process.
* @returns
*/
async getProcessDefinitionXML(processDefinitionKey) {
return this.callApiEndpoint({
method: 'GET',
json: false,
urlPath: `process-definitions/${processDefinitionKey}/xml`,
});
}
async getProcessDefinition(processDefinitionKey) {
return this.callApiEndpoint({
method: 'GET',
urlPath: `process-definitions/${processDefinitionKey}`,
});
}
/**
* @description Search for process definitions based on given criteria.
* Documentation: https://docs.camunda.io/docs/next/apis-tools/camunda-api-rest/specifications/search-process-definitions/
* @since 8.8.0
*/
async searchProcessDefinitions(request) {
return this.callApiEndpoint({
method: 'POST',
urlPath: `process-definitions/search`,
body: request,
});
}
/**
* @description Search for element instances based on given criteria.
* Documentation: https://docs.camunda.io/docs/next/apis-tools/camunda-api-rest/specifications/search-element-instances/
* @since 8.8.0
*/
async searchElementInstances(request) {
return this.callApiEndpoint({
method: 'POST',
urlPath: `element-instances/search`,
body: request,
});
}
/**
* @description Returns element instance as JSON.
* Documentation: https://docs.camunda.io/docs/next/apis-tools/camunda-api-rest/specifications/get-element-instance/
* @since 8.8.0
*/
async getElementInstance(elementInstanceKey) {
return this.callApiEndpoint({
method: 'GET',
urlPath: `element-instances/${elementInstanceKey}`,
});
}
/**
* @description Search for incidents based on given criteria.
* Documentation: https://docs.camunda.io/docs/next/apis-tools/camunda-api-rest/specifications/search-incidents/
* @since 8.8.0
*/
async searchIncidents(request) {
return this.callApiEndpoint({
method: 'POST',
urlPath: `incidents/search`,
body: request,
});
}
callApiEndpoint(request) {
// Return a cancelable promise
// https://github.com/camunda/camunda-8-js-sdk/issues/424
return new p_cancelable_1.default(async (resolve, reject, onCancel) => {
// eslint-disable-next-line prefer-const
let gotRequest;
let cancelled = false;
// Register the cancel handler, we will cancel the request if the promise is cancelled
onCancel.shouldReject = false;
onCancel(() => {
cancelled = true;
if (gotRequest !== undefined && !gotRequest.isCanceled) {
gotRequest.cancel(`REST operation cancelled`);
}
});
try {
const headers = await this.getHeaders();
const { method, urlPath, body, queryParams } = request;
trace(`Constructing request for API endpoint: [${method}] ${urlPath}`);
const req = {
method,
headers: request.headers
? { ...headers, ...request.headers }
: headers,
body: body ? (0, lib_1.losslessStringify)(body) : undefined,
searchParams: queryParams ?? {},
parseJson: request.parseJson ?? lib_1.losslessParse,
};
const rest = await this.rest;
if (cancelled) {
// If the request was already cancelled, we don't want to send it
return;
}
gotRequest = rest(urlPath, req);
gotRequest.catch(reject);
(await gotRequest).once('error', (err) => {
// Swallow the error, we don't want to crash the process
return err;