UNPKG

@camunda8/sdk

Version:

[![NPM](https://nodei.co/npm/@camunda8/sdk.png)](https://www.npmjs.com/package/@camunda8/sdk)

1,180 lines (1,179 loc) 50.8 kB
"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;