diff --git a/src/c8/index.ts b/src/c8/index.ts index 2c6b55a7..a0cceaeb 100644 --- a/src/c8/index.ts +++ b/src/c8/index.ts @@ -12,6 +12,8 @@ import { OptimizeApiClient } from '../optimize' import { TasklistApiClient } from '../tasklist' import { ZeebeGrpcClient, ZeebeRestClient } from '../zeebe' +import { C8RestClient } from './lib/C8RestClient' + /** * A single point of configuration for all Camunda Platform 8 clients. * @@ -23,12 +25,12 @@ import { ZeebeGrpcClient, ZeebeRestClient } from '../zeebe' * * const c8 = new Camunda8() * const zeebe = c8.getZeebeGrpcApiClient() - * const zeebeRest = c8.getZeebeRestClient() * const operate = c8.getOperateApiClient() * const optimize = c8.getOptimizeApiClient() * const tasklist = c8.getTasklistApiClient() * const modeler = c8.getModelerApiClient() * const admin = c8.getAdminApiClient() + * const c8Rest = c8.getC8RestClient() * ``` */ export class Camunda8 { @@ -41,6 +43,7 @@ export class Camunda8 { private zeebeRestClient?: ZeebeRestClient private configuration: CamundaPlatform8Configuration private oAuthProvider: IOAuthProvider + private c8RestClient?: C8RestClient constructor(config: DeepPartial = {}) { this.configuration = @@ -108,6 +111,9 @@ export class Camunda8 { return this.zeebeGrpcApiClient } + /** + * @deprecated from 8.6. Please use getC8RestClient() instead. + */ public getZeebeRestClient(): ZeebeRestClient { if (!this.zeebeRestClient) { this.zeebeRestClient = new ZeebeRestClient({ @@ -117,4 +123,14 @@ export class Camunda8 { } return this.zeebeRestClient } + + public getC8RestClient(): C8RestClient { + if (!this.c8RestClient) { + this.c8RestClient = new C8RestClient({ + config: this.configuration, + oAuthProvider: this.oAuthProvider, + }) + } + return this.c8RestClient + } } diff --git a/src/c8/lib/C8Dto.ts b/src/c8/lib/C8Dto.ts new file mode 100644 index 00000000..3ebfc90d --- /dev/null +++ b/src/c8/lib/C8Dto.ts @@ -0,0 +1,57 @@ +import { LosslessNumber } from 'lossless-json' + +import { Int64String, LosslessDto } from '../../lib' +import { JSONDoc } from '../../zeebe/types' + +export class Job extends LosslessDto { + @Int64String + key!: string + type!: string + @Int64String + processInstanceKey!: LosslessNumber + bpmnProcessId!: string + processDefinitionVersion!: number + @Int64String + processDefinitionKey!: LosslessNumber + elementId!: string + @Int64String + elementInstanceKey!: LosslessNumber + customHeaders!: T + worker!: string + retries!: number + @Int64String + deadline!: LosslessNumber + variables!: JSONDoc + tenantId!: string +} + +/** + * JSON object with changed task attribute values. + */ +export interface TaskChangeSet { + /* The due date of the task. Reset by providing an empty String. */ + dueDate?: Date | string + /* The follow-up date of the task. Reset by providing an empty String. */ + followUpDate?: Date | string + /* The list of candidate users of the task. Reset by providing an empty list. */ + candidateUsers?: string[] + /* The list of candidate groups of the task. Reset by providing an empty list. */ + candidateGroups?: string[] +} + +/** JSON object with changed job attribute values. */ +export interface JobUpdateChangeset { + /* The new amount of retries for the job; must be a positive number. */ + retries?: number + /** The duration of the new timeout in ms, starting from the current moment. */ + timeout?: number +} + +export interface NewUserInfo { + password: string + id: number + username: string + name: string + email: string + enabled: boolean +} diff --git a/src/c8/lib/C8RestClient.ts b/src/c8/lib/C8RestClient.ts new file mode 100644 index 00000000..a1c73efe --- /dev/null +++ b/src/c8/lib/C8RestClient.ts @@ -0,0 +1,343 @@ +import { debug } from 'debug' +import got from 'got' + +import { + CamundaEnvironmentConfigurator, + CamundaPlatform8Configuration, + DeepPartial, + GetCustomCertificateBuffer, + GotRetryConfig, + LosslessDto, + RequireConfiguration, + constructOAuthProvider, + createUserAgentString, + gotBeforeErrorHook, + gotErrorHandler, + losslessParse, + losslessStringify, + makeBeforeRetryHandlerFor401TokenRetry, +} from '../../lib' +import { IOAuthProvider } from '../../oauth' +import { + ActivateJobsRequest, + CompleteJobRequest, + ErrorJobWithVariables, + FailJobRequest, + PublishMessageRequest, + PublishMessageResponse, + TopologyResponse, +} from '../../zeebe/types' + +import { Job, JobUpdateChangeset, NewUserInfo, TaskChangeSet } from './C8Dto' + +const trace = debug('camunda:zeebe') + +const CAMUNDA_REST_API_VERSION = 'v2' + +export class C8RestClient { + private userAgentString: string + private oAuthProvider: IOAuthProvider + private rest: Promise + private tenantId?: string + + constructor(options?: { + config?: DeepPartial + oAuthProvider?: IOAuthProvider + }) { + const config = CamundaEnvironmentConfigurator.mergeConfigWithEnvironment( + options?.config ?? {} + ) + trace('options.config', options?.config) + trace('config', config) + this.oAuthProvider = + options?.oAuthProvider ?? constructOAuthProvider(config) + this.userAgentString = createUserAgentString(config) + this.tenantId = config.CAMUNDA_TENANT_ID + const baseUrl = RequireConfiguration( + config.ZEEBE_REST_ADDRESS, + 'ZEEBE_REST_ADDRESS' + ) + + const prefixUrl = `${baseUrl}/${CAMUNDA_REST_API_VERSION}` + + this.rest = GetCustomCertificateBuffer(config).then( + (certificateAuthority) => + got.extend({ + prefixUrl, + retry: GotRetryConfig, + https: { + certificateAuthority, + }, + handlers: [gotErrorHandler], + hooks: { + beforeRetry: [ + makeBeforeRetryHandlerFor401TokenRetry( + this.getHeaders.bind(this) + ), + ], + beforeError: [gotBeforeErrorHook], + }, + }) + ) + + // this.tenantId = config.CAMUNDA_TENANT_ID + } + + private async getHeaders() { + const token = await this.oAuthProvider.getToken('ZEEBE') + + const headers = { + 'content-type': 'application/json', + authorization: `Bearer ${token}`, + 'user-agent': this.userAgentString, + accept: '*/*', + } + trace('headers', headers) + return headers + } + + /* Get the topology of the Zeebe cluster. */ + public async getTopology(): Promise { + const headers = await this.getHeaders() + return this.rest.then((rest) => + rest + .get('topology', { headers }) + .json() + .catch((error) => { + trace('error', error) + throw error + }) + ) as Promise + } + + /* Completes 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/ */ + public async completeUserTask({ + userTaskKey, + variables = {}, + action = 'complete', + }: { + userTaskKey: string + variables?: Record + action?: string + }) { + const headers = await this.getHeaders() + return this.rest.then((rest) => + rest.post(`user-tasks/${userTaskKey}/completion`, { + body: losslessStringify({ + variables, + action, + }), + headers, + }) + ) + } + + /* Assigns a user task with the given key to the given assignee. */ + public async assignTask({ + userTaskKey, + assignee, + allowOverride = true, + action = 'assign', + }: { + userTaskKey: string + assignee: string + allowOverride?: boolean + action: string + }) { + const headers = await this.getHeaders() + + return this.rest.then((rest) => + rest.post(`user-tasks/${userTaskKey}/assignment`, { + body: losslessStringify({ + allowOverride, + action, + assignee, + }), + headers, + }) + ) + } + + /** Update a user task with the given key. */ + public async updateTask({ + userTaskKey, + changeset, + }: { + userTaskKey: string + changeset: TaskChangeSet + }) { + const headers = await this.getHeaders() + + return this.rest.then((rest) => + rest.patch(`user-tasks/${userTaskKey}/update`, { + body: losslessStringify(changeset), + headers, + }) + ) + } + /* Removes the assignee of a task with the given key. */ + public async unassignTask({ userTaskKey }: { userTaskKey: string }) { + const headers = await this.getHeaders() + + return this.rest.then((rest) => + rest.delete(`user-tasks/${userTaskKey}/assignee`, { headers }) + ) + } + + /** + * Create a user + */ + public async createUser(newUserInfo: NewUserInfo) { + const headers = await this.getHeaders() + + return this.rest.then((rest) => + rest.post(`users`, { + body: JSON.stringify(newUserInfo), + headers, + }) + ) + } + + /** + * Search for user tasks based on given criteria. + * @experimental + */ + public async queryTasks() {} + + /** + * Publishes a Message and correlates it to a subscription. If correlation is successful it + * will return the first process instance key the message correlated with. + **/ + public async correlateMessage( + message: Pick< + PublishMessageRequest, + 'name' | 'correlationKey' | 'variables' | 'tenantId' + > + ): Promise { + const headers = await this.getHeaders() + + return this.rest.then((rest) => + rest + .post(`messages/correlation`, { + body: losslessStringify(message), + headers, + }) + .json() + ) + } + + /** + * Obtains the status of the current Camunda license + */ + public async getLicenseStatus(): Promise<{ + vaildLicense: boolean + licenseType: string + }> { + return this.rest.then((rest) => rest.get(`license`).json()) + } + + /** + * Iterate through all known partitions and activate jobs up to the requested maximum. + * + * The type parameter T specifies the type of the job payload. This can be set to a class that extends LosslessDto to provide + * both type information in your code, and safe interoperability with other applications that natively support the int64 type. + */ + public async activateJobs( + request: ActivateJobsRequest + ): Promise { + const headers = await this.getHeaders() + + return this.rest.then((rest) => + rest + .post(`jobs/activation`, { + body: losslessStringify(this.addDefaultTenantId(request)), + headers, + parseJson: (text) => losslessParse(text, Job), + }) + .json() + ) + } + + /** + * 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. + */ + public async failJob(failJobRequest: FailJobRequest) { + const { jobKey } = failJobRequest + const headers = await this.getHeaders() + + return this.rest.then((rest) => + rest.post(`jobs/${jobKey}/fail`, { + body: losslessStringify(failJobRequest), + headers, + }) + ) + } + + /** + * Reports a business error (i.e. non-technical) that occurs while processing a job. + */ + public async errorJob( + errorJobRequest: ErrorJobWithVariables & { jobKey: string } + ) { + const { jobKey, ...request } = errorJobRequest + const headers = await this.getHeaders() + + return this.rest.then((rest) => + rest.post(`jobs/${jobKey}/error`, { + body: losslessStringify(request), + headers, + }) + ) + } + + /** + * Complete a job with the given payload, which allows completing the associated service task. + */ + public async completeJob(completeJobRequest: CompleteJobRequest) { + const { jobKey } = completeJobRequest + const headers = await this.getHeaders() + + return this.rest.then((rest) => + rest.post(`jobs/${jobKey}/complete`, { + body: losslessStringify({ variables: completeJobRequest.variables }), + headers, + }) + ) + } + + /** + * Update a job with the given key. + */ + public async updateJob( + jobChangeset: JobUpdateChangeset & { jobKey: string } + ) { + const { jobKey, ...changeset } = jobChangeset + const headers = await this.getHeaders() + + return this.rest.then((rest) => + rest.patch(`jobs/${jobKey}`, { + body: JSON.stringify(changeset), + headers, + }) + ) + } + + public async resolveIncident(incidentKey: string) { + const headers = await this.getHeaders() + + return this.rest.then((rest) => + rest.post(`incidents/${incidentKey}/resolve`, { + headers, + }) + ) + } + + /** + * Helper method to add the default tenantIds if we are not passed explicit tenantIds + */ + private addDefaultTenantId(request: T) { + const tenantIds = request.tenantIds ?? this.tenantId ? [this.tenantId] : [] + return { ...request, tenantIds } + } +} diff --git a/src/zeebe/zb/ZeebeRESTClient.ts b/src/zeebe/zb/ZeebeRESTClient.ts index a0e41e84..28d73093 100644 --- a/src/zeebe/zb/ZeebeRESTClient.ts +++ b/src/zeebe/zb/ZeebeRESTClient.ts @@ -35,6 +35,9 @@ interface TaskChangeSet { candidateGroups?: string[] } +/** + * @deprecated Since 8.6. Please use `C8RestClient` instead. + */ export class ZeebeRestClient { private userAgentString: string private oAuthProvider: IOAuthProvider