From 2e5af662f9ecc1ee23115eb4232e0633afde3efe Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Fri, 30 Aug 2024 16:57:34 +1200 Subject: [PATCH 01/34] feat(zeebe): add operationReference field to gRPC methods fixes #237 --- src/proto/zeebe.proto | 22 ++++++++ src/zeebe/lib/ZBWorkerBase.ts | 3 ++ src/zeebe/lib/interfaces-1.0.ts | 55 ++++++++++++++++++++ src/zeebe/lib/interfaces-grpc-1.0.ts | 23 ++++++++- src/zeebe/zb/ZeebeGrpcClient.ts | 77 +++++++++++++++++++++------- 5 files changed, 160 insertions(+), 20 deletions(-) diff --git a/src/proto/zeebe.proto b/src/proto/zeebe.proto index 0333ff8e..fd9bf17e 100644 --- a/src/proto/zeebe.proto +++ b/src/proto/zeebe.proto @@ -89,6 +89,8 @@ message CancelProcessInstanceRequest { // the process instance key (as, for example, obtained from // CreateProcessInstanceResponse) int64 processInstanceKey = 1; + // a reference key chosen by the user and will be part of all records resulted from this operation + optional uint64 operationReference = 2; } message CancelProcessInstanceResponse { @@ -124,6 +126,9 @@ message CreateProcessInstanceRequest { repeated ProcessInstanceCreationStartInstruction startInstructions = 5; // the tenant id of the process definition string tenantId = 6; + + // a reference key chosen by the user and will be part of all records resulted from this operation + optional uint64 operationReference = 7; } message ProcessInstanceCreationStartInstruction { @@ -481,6 +486,8 @@ message PublishMessageResponse { message ResolveIncidentRequest { // the unique ID of the incident to resolve int64 incidentKey = 1; + // a reference key chosen by the user and will be part of all records resulted from this operation + optional uint64 operationReference = 2; } message ResolveIncidentResponse { @@ -543,6 +550,8 @@ message UpdateJobRetriesRequest { int64 jobKey = 1; // the new amount of retries for the job; must be positive int32 retries = 2; + // a reference key chosen by the user and will be part of all records resulted from this operation + optional uint64 operationReference = 3; } message UpdateJobRetriesResponse { @@ -553,6 +562,8 @@ message UpdateJobTimeoutRequest { int64 jobKey = 1; // the duration of the new timeout in ms, starting from the current moment int64 timeout = 2; + // a reference key chosen by the user and will be part of all records resulted from this operation + optional uint64 operationReference = 3; } message UpdateJobTimeoutResponse { @@ -574,6 +585,8 @@ message SetVariablesRequest { // be unchanged, and scope 2 will now be `{ "bar" : 1, "foo" 5 }`. if local was false, however, // then scope 1 would be `{ "foo": 5 }`, and scope 2 would be `{ "bar" : 1 }`. bool local = 3; + // a reference key chosen by the user and will be part of all records resulted from this operation + optional uint64 operationReference = 4; } message SetVariablesResponse { @@ -589,6 +602,8 @@ message ModifyProcessInstanceRequest { repeated ActivateInstruction activateInstructions = 2; // instructions describing which elements should be terminated repeated TerminateInstruction terminateInstructions = 3; + // a reference key chosen by the user and will be part of all records resulted from this operation + optional uint64 operationReference = 4; message ActivateInstruction { // the id of the element that should be activated @@ -628,6 +643,8 @@ message MigrateProcessInstanceRequest { int64 processInstanceKey = 1; // the migration plan that defines target process and element mappings MigrationPlan migrationPlan = 2; + // a reference key chosen by the user and will be part of all records resulted from this operation + optional uint64 operationReference = 3; message MigrationPlan { // the key of process definition to migrate the process instance to @@ -652,6 +669,8 @@ message DeleteResourceRequest { // The key of the resource that should be deleted. This can either be the key // of a process definition, the key of a decision requirements definition or the key of a form. int64 resourceKey = 1; + // a reference key chosen by the user and will be part of all records resulted from this operation + optional uint64 operationReference = 2; } message DeleteResourceResponse { @@ -934,9 +953,12 @@ service Gateway { FAILED_PRECONDITION: - not all active elements in the given process instance are mapped to the elements in the target process definition - a mapping instruction changes the type of an element or event + - a mapping instruction changes the implementation of a task + - a mapping instruction detaches a boundary event from an active element - a mapping instruction refers to an unsupported element (i.e. some elements will be supported later on) - a mapping instruction refers to element in unsupported scenarios. (i.e. migration is not supported when process instance or target process elements contains event subscriptions) + - multiple mapping instructions target the same boundary event INVALID_ARGUMENT: - A `sourceElementId` does not refer to an element in the process instance's process definition diff --git a/src/zeebe/lib/ZBWorkerBase.ts b/src/zeebe/lib/ZBWorkerBase.ts index 730c1f65..6c402ad6 100644 --- a/src/zeebe/lib/ZBWorkerBase.ts +++ b/src/zeebe/lib/ZBWorkerBase.ts @@ -380,11 +380,13 @@ You should call only one job action method in the worker handler. This is a bug errorMessage, retries, retryBackOff, + variables, }: { job: ZB.Job errorMessage: string retries?: number retryBackOff?: number + variables?: ZB.JSONDoc }) { return this.zbClient .failJob({ @@ -392,6 +394,7 @@ You should call only one job action method in the worker handler. This is a bug jobKey: job.key, retries: retries ?? job.retries - 1, retryBackOff: retryBackOff ?? 0, + variables: variables ?? {}, }) .then(() => ZB.JOB_ACTION_ACKNOWLEDGEMENT) .finally(() => { diff --git a/src/zeebe/lib/interfaces-1.0.ts b/src/zeebe/lib/interfaces-1.0.ts index 31719fac..38b71ce1 100644 --- a/src/zeebe/lib/interfaces-1.0.ts +++ b/src/zeebe/lib/interfaces-1.0.ts @@ -1,9 +1,11 @@ import { ClientReadableStream } from '@grpc/grpc-js' import { Chalk } from 'chalk' +import { LosslessNumber } from 'lossless-json' import { MaybeTimeDuration } from 'typed-duration' import { GrpcClient } from './GrpcClient' import { + ActivateInstruction, ActivateJobsRequest, BroadcastSignalRequest, BroadcastSignalResponse, @@ -20,6 +22,7 @@ import { FailJobRequest, MigrateProcessInstanceRequest, MigrateProcessInstanceResponse, + MigrationPlan, ModifyProcessInstanceRequest, ModifyProcessInstanceResponse, ProcessInstanceCreationStartInstruction, @@ -28,6 +31,7 @@ import { ResolveIncidentRequest, SetVariablesRequestOnTheWire, StreamActivatedJobsRequest, + TerminateInstruction, ThrowErrorRequest, TopologyResponse, UpdateJobRetriesRequest, @@ -61,6 +65,8 @@ export interface CreateProcessBaseRequest { variables: V /** The tenantId for a multi-tenant enabled cluster. */ tenantId?: string + /** a reference key chosen by the user and will be part of all records resulted from this operation */ + operationReference?: number | LosslessNumber } export interface CreateProcessInstanceReq @@ -133,6 +139,10 @@ export interface JobFailureConfiguration { * Optional backoff for subsequent retries, in milliseconds. If not specified, it is zero. */ retryBackOff?: number + /** + * Optional variable update for the job + */ + variables?: JSONDoc } declare function FailureHandler( @@ -400,6 +410,50 @@ export interface BroadcastSignalRes { key: string } +export interface ResolveIncidentReq { + readonly incidentKey: string + /** a reference key chosen by the user and will be part of all records resulted from this operation */ + operationReference?: number | LosslessNumber +} + +export interface UpdateJobRetriesReq { + readonly jobKey: string + retries: number + /** a reference key chosen by the user and will be part of all records resulted from this operation */ + operationReference?: number | LosslessNumber +} + +export interface UpdateJobTimeoutReq { + readonly jobKey: string + /** the duration of the new timeout in ms, starting from the current moment */ + timeout: number + /** a reference key chosen by the user and will be part of all records resulted from this operation */ + operationReference?: number | LosslessNumber +} + +export interface ModifyProcessInstanceReq { + /** the key of the process instance that should be modified */ + processInstanceKey: string + /** + * instructions describing which elements should be activated in which scopes, + * and which variables should be created + */ + activateInstructions?: ActivateInstruction[] + /** instructions describing which elements should be terminated */ + terminateInstructions?: TerminateInstruction[] + /** a reference key chosen by the user and will be part of all records resulted from this operation */ + operationReference?: number | LosslessNumber +} + +export interface MigrateProcessInstanceReq { + // key of the process instance to migrate + processInstanceKey: string + // the migration plan that defines target process and element mappings + migrationPlan: MigrationPlan + /** a reference key chosen by the user and will be part of all records resulted from this operation */ + operationReference?: string +} + export interface ZBGrpc extends GrpcClient { completeJobSync: (req: CompleteJobRequest) => Promise activateJobsStream: ( @@ -434,6 +488,7 @@ export interface ZBGrpc extends GrpcClient { ): Promise cancelProcessInstanceSync(processInstanceKey: { processInstanceKey: string | number + operationReference?: string }): Promise migrateProcessInstanceSync( request: MigrateProcessInstanceRequest diff --git a/src/zeebe/lib/interfaces-grpc-1.0.ts b/src/zeebe/lib/interfaces-grpc-1.0.ts index 16b4f89b..3b267eed 100644 --- a/src/zeebe/lib/interfaces-grpc-1.0.ts +++ b/src/zeebe/lib/interfaces-grpc-1.0.ts @@ -1,3 +1,4 @@ +import { LosslessNumber } from 'lossless-json' import { MaybeTimeDuration } from 'typed-duration' import { IInputVariables, IProcessVariables, JSONDoc } from './interfaces-1.0' @@ -142,6 +143,8 @@ export interface CreateProcessInstanceRequest * instructions after it has been created */ startInstructions: ProcessInstanceCreationStartInstruction[] + /** a reference key chosen by the user and will be part of all records resulted from this operation */ + operationReference?: string } export interface ProcessInstanceCreationStartInstruction { @@ -463,12 +466,16 @@ export interface PublishStartMessageRequest { export interface UpdateJobRetriesRequest { readonly jobKey: string retries: number + /** a reference key chosen by the user and will be part of all records resulted from this operation */ + operationReference?: string } export interface UpdateJobTimeoutRequest { readonly jobKey: string /** the duration of the new timeout in ms, starting from the current moment */ timeout: number + /** a reference key chosen by the user and will be part of all records resulted from this operation */ + operationReference?: string } export interface FailJobRequest { @@ -476,6 +483,7 @@ export interface FailJobRequest { retries: number errorMessage: string retryBackOff: number + variables?: JSONDoc } export interface ThrowErrorRequest { @@ -521,14 +529,19 @@ interface SetVariablesRequestBase { export interface SetVariablesRequest extends SetVariablesRequestBase { variables: Partial + /** a reference key chosen by the user and will be part of all records resulted from this operation */ + operationReference?: number | LosslessNumber } export interface SetVariablesRequestOnTheWire extends SetVariablesRequestBase { variables: string + operationReference?: string } export interface ResolveIncidentRequest { readonly incidentKey: string + /** a reference key chosen by the user and will be part of all records resulted from this operation */ + operationReference?: string } export interface ActivateInstruction { @@ -575,6 +588,8 @@ export interface ModifyProcessInstanceRequest { activateInstructions?: ActivateInstruction[] /** instructions describing which elements should be terminated */ terminateInstructions?: TerminateInstruction[] + /** a reference key chosen by the user and will be part of all records resulted from this operation */ + operationReference?: string } export type ModifyProcessInstanceResponse = Record @@ -584,16 +599,18 @@ export interface MigrateProcessInstanceRequest { processInstanceKey: string // the migration plan that defines target process and element mappings migrationPlan: MigrationPlan + /** a reference key chosen by the user and will be part of all records resulted from this operation */ + operationReference?: string } -interface MigrationPlan { +export interface MigrationPlan { // the key of process definition to migrate the process instance to targetProcessDefinitionKey: string // the mapping instructions describe how to map elements from the source process definition to the target process definition mappingInstructions: MappingInstruction[] } -interface MappingInstruction { +export interface MappingInstruction { // the element id to migrate from sourceElementId: string // the element id to migrate into @@ -743,6 +760,8 @@ export interface DeleteResourceRequest { * of a process definition, the key of a decision requirements definition or the key of a form. */ resourceKey: string + /** a reference key chosen by the user and will be part of all records resulted from this operation */ + operationReference?: number | LosslessNumber } export interface BroadcastSignalRequest { diff --git a/src/zeebe/zb/ZeebeGrpcClient.ts b/src/zeebe/zb/ZeebeGrpcClient.ts index 0e28649a..7d57d832 100644 --- a/src/zeebe/zb/ZeebeGrpcClient.ts +++ b/src/zeebe/zb/ZeebeGrpcClient.ts @@ -3,6 +3,7 @@ import * as path from 'path' import chalk from 'chalk' import d from 'debug' +import { LosslessNumber } from 'lossless-json' import promiseRetry from 'promise-retry' import { Duration, MaybeTimeDuration } from 'typed-duration' import { v4 as uuid } from 'uuid' @@ -10,12 +11,12 @@ import { v4 as uuid } from 'uuid' import { CamundaEnvironmentConfigurator, CamundaPlatform8Configuration, + constructOAuthProvider, DeepPartial, GetCustomCertificateBuffer, LosslessDto, - RequireConfiguration, - constructOAuthProvider, losslessStringify, + RequireConfiguration, } from '../../lib' import { IOAuthProvider } from '../../oauth' import { @@ -33,7 +34,7 @@ import { StatefulLogInterceptor } from '../lib/StatefulLogInterceptor' import { TypedEmitter } from '../lib/TypedEmitter' import { ZBJsonLogger } from '../lib/ZBJsonLogger' import { ZBStreamWorker } from '../lib/ZBStreamWorker' -import { Resource, getResourceContentAndName } from '../lib/deployResource' +import { getResourceContentAndName, Resource } from '../lib/deployResource' import * as ZB from '../lib/interfaces-1.0' import { ZBWorkerTaskHandler } from '../lib/interfaces-1.0' import * as Grpc from '../lib/interfaces-grpc-1.0' @@ -350,12 +351,15 @@ export class ZeebeGrpcClient extends TypedEmitter< * ``` */ public async cancelProcessInstance( - processInstanceKey: string | number + processInstanceKey: string, + operationReference?: number | LosslessNumber ): Promise { Utils.validateNumber(processInstanceKey, 'processInstanceKey') + const parsedOperationReference = operationReference?.toString() ?? undefined return this.executeOperation('cancelProcessInstance', async () => (await this.grpc).cancelProcessInstanceSync({ processInstanceKey, + operationReference: parsedOperationReference, }) ) } @@ -582,6 +586,8 @@ export class ZeebeGrpcClient extends TypedEmitter< >( config: ZB.CreateProcessInstanceReq ): Promise { + const operationReference = + config.operationReference?.toString() ?? undefined const request: ZB.CreateProcessInstanceReq = { bpmnProcessId: config.bpmnProcessId, variables: config.variables, @@ -592,6 +598,7 @@ export class ZeebeGrpcClient extends TypedEmitter< const createProcessInstanceRequest: Grpc.CreateProcessInstanceRequest = stringifyVariables({ ...request, + operationReference, startInstructions: request.startInstructions!, tenantId: config.tenantId ?? this.tenantId, }) @@ -662,11 +669,13 @@ export class ZeebeGrpcClient extends TypedEmitter< */ deleteResource({ resourceKey, + operationReference, }: { resourceKey: string + operationReference?: number | LosslessNumber }): Promise> { return this.executeOperation('deleteResourceSync', async () => - (await this.grpc).deleteResourceSync({ resourceKey }) + (await this.grpc).deleteResourceSync({ resourceKey, operationReference }) ) } @@ -827,8 +836,14 @@ export class ZeebeGrpcClient extends TypedEmitter< * ``` */ public failJob(failJobRequest: Grpc.FailJobRequest): Promise { + const variables = failJobRequest.variables ? failJobRequest.variables : {} + const withStringifiedVariables = stringifyVariables({ + ...failJobRequest, + variables, + }) + return this.executeOperation('failJob', async () => - (await this.grpc).failJobSync(failJobRequest) + (await this.grpc).failJobSync(withStringifiedVariables) ) } @@ -869,8 +884,10 @@ export class ZeebeGrpcClient extends TypedEmitter< * ``` */ public modifyProcessInstance( - modifyProcessInstanceRequest: Grpc.ModifyProcessInstanceRequest + modifyProcessInstanceRequest: ZB.ModifyProcessInstanceReq ): Promise { + const operationReference = + modifyProcessInstanceRequest.operationReference?.toString() return this.executeOperation('modifyProcessInstance', async () => { // We accept JSONDoc for the variableInstructions, but the actual gRPC call needs stringified JSON, so transform it with a mutation const req = Utils.deepClone(modifyProcessInstanceRequest) @@ -881,6 +898,7 @@ export class ZeebeGrpcClient extends TypedEmitter< ) return (await this.grpc).modifyProcessInstanceSync({ ...req, + operationReference, }) }) } @@ -890,12 +908,15 @@ export class ZeebeGrpcClient extends TypedEmitter< * @since 8.5.0 */ public migrateProcessInstance( - migrateProcessInstanceRequest: Grpc.MigrateProcessInstanceRequest + migrateProcessInstanceRequest: ZB.MigrateProcessInstanceReq ): Promise { + const operationReference = + migrateProcessInstanceRequest.operationReference?.toString() return this.executeOperation('migrateProcessInstance', async () => - (await this.grpc).migrateProcessInstanceSync( - migrateProcessInstanceRequest - ) + (await this.grpc).migrateProcessInstanceSync({ + ...migrateProcessInstanceRequest, + operationReference, + }) ) } @@ -1027,10 +1048,15 @@ export class ZeebeGrpcClient extends TypedEmitter< * ``` */ public resolveIncident( - resolveIncidentRequest: Grpc.ResolveIncidentRequest + resolveIncidentRequest: ZB.ResolveIncidentReq ): Promise { + const operationReference = + resolveIncidentRequest.operationReference?.toString() return this.executeOperation('resolveIncident', async () => - (await this.grpc).resolveIncidentSync(resolveIncidentRequest) + (await this.grpc).resolveIncidentSync({ + ...resolveIncidentRequest, + operationReference, + }) ) } @@ -1080,8 +1106,13 @@ export class ZeebeGrpcClient extends TypedEmitter< ? losslessStringify(request.variables) : request.variables + const operationReference = request.operationReference?.toString() return this.executeOperation('setVariables', async () => - (await this.grpc).setVariablesSync({ ...request, variables }) + (await this.grpc).setVariablesSync({ + ...request, + variables, + operationReference, + }) ) } @@ -1315,10 +1346,15 @@ export class ZeebeGrpcClient extends TypedEmitter< * ``` */ public updateJobRetries( - updateJobRetriesRequest: Grpc.UpdateJobRetriesRequest + updateJobRetriesRequest: ZB.UpdateJobRetriesReq ): Promise { + const operationReference = + updateJobRetriesRequest.operationReference?.toString() return this.executeOperation('updateJobRetries', async () => - (await this.grpc).updateJobRetriesSync(updateJobRetriesRequest) + (await this.grpc).updateJobRetriesSync({ + ...updateJobRetriesRequest, + operationReference, + }) ) } @@ -1334,10 +1370,15 @@ export class ZeebeGrpcClient extends TypedEmitter< - no deadline exists for the given job key */ public updateJobTimeout( - updateJobTimeoutRequest: Grpc.UpdateJobTimeoutRequest + updateJobTimeoutRequest: ZB.UpdateJobTimeoutReq ): Promise { + const operationReference = + updateJobTimeoutRequest.operationReference?.toString() return this.executeOperation('updateJobTimeout', async () => - (await this.grpc).updateJobTimeoutSync(updateJobTimeoutRequest) + (await this.grpc).updateJobTimeoutSync({ + ...updateJobTimeoutRequest, + operationReference, + }) ) } From 8e93c92bfb3684b530428f253f5de05c771e4215 Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Fri, 30 Aug 2024 17:25:41 +1200 Subject: [PATCH 02/34] feat(camunda8): add C8RestClient support 8.6 unified C8 REST API, deprecate ZeebeRestClient fixes #235 --- src/c8/index.ts | 18 +- src/c8/lib/C8Dto.ts | 57 ++++++ src/c8/lib/C8RestClient.ts | 343 ++++++++++++++++++++++++++++++++ src/zeebe/zb/ZeebeRESTClient.ts | 3 + 4 files changed, 420 insertions(+), 1 deletion(-) create mode 100644 src/c8/lib/C8Dto.ts create mode 100644 src/c8/lib/C8RestClient.ts 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 From f19a2520778836c34a3685d584c4969380672804 Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Fri, 6 Sep 2024 13:26:15 +1200 Subject: [PATCH 03/34] feat(zeebe): lossless parse REST variables and customheaders fixes #244 --- package-lock.json | 2 +- src/__tests__/c8/rest/activateJobs.spec.ts | 52 ++++++++ src/__tests__/c8/rest/parseJobs.spec.ts | 23 ++++ .../lib/LosslessJsonParser.unit.spec.ts | 8 ++ src/c8/lib/C8Dto.ts | 13 +- src/c8/lib/C8RestClient.ts | 118 ++++++++++++++---- src/c8/lib/RestApiJobClassFactory.ts | 67 ++++++++++ src/lib/LosslessJsonParser.ts | 8 +- src/lib/test.ts | 54 ++++++++ src/zeebe/lib/interfaces-1.0.ts | 38 ++++++ 10 files changed, 351 insertions(+), 32 deletions(-) create mode 100644 src/__tests__/c8/rest/activateJobs.spec.ts create mode 100644 src/__tests__/c8/rest/parseJobs.spec.ts create mode 100644 src/c8/lib/RestApiJobClassFactory.ts create mode 100644 src/lib/test.ts diff --git a/package-lock.json b/package-lock.json index ff0c6094..a7959ff1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,7 @@ "": { "name": "@camunda8/sdk", "version": "8.6.2", - "license": "Apache 2.0", + "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "1.10.9", "@grpc/proto-loader": "0.7.13", diff --git a/src/__tests__/c8/rest/activateJobs.spec.ts b/src/__tests__/c8/rest/activateJobs.spec.ts new file mode 100644 index 00000000..4d9d5050 --- /dev/null +++ b/src/__tests__/c8/rest/activateJobs.spec.ts @@ -0,0 +1,52 @@ +import { C8RestClient } from '../../../c8/lib/C8RestClient' +import { restoreZeebeLogging, suppressZeebeLogging } from '../../../lib' +import { ZeebeGrpcClient } from '../../../zeebe' +import { DeployResourceResponse, ProcessDeployment } from '../../../zeebe/types' + +suppressZeebeLogging() +let res: DeployResourceResponse +let bpmnProcessId: string +const grpcClient = new ZeebeGrpcClient({ + config: { + CAMUNDA_TENANT_ID: '', + }, +}) +const restClient = new C8RestClient() + +beforeAll(async () => { + res = await grpcClient.deployResource({ + processFilename: './src/__tests__/testdata/hello-world-complete.bpmn', + }) + bpmnProcessId = res.deployments[0].process.bpmnProcessId +}) + +afterAll(async () => { + restoreZeebeLogging() + await grpcClient.close() +}) + +test('Can service a task', (done) => { + grpcClient + .createProcessInstance({ + bpmnProcessId, + variables: {}, + }) + .then((r) => { + console.log(r) + restClient + .activateJobs({ + maxJobsToActivate: 2, + requestTimeout: 5000, + timeout: 5000, + type: 'console-log-complete', + worker: 'test', + }) + .then((jobs) => { + expect(jobs.length).toBe(1) + console.log(jobs) + const res = jobs.map((job) => job.complete()) + console.log(res) + Promise.all(res).then(() => done()) + }) + }) +}) diff --git a/src/__tests__/c8/rest/parseJobs.spec.ts b/src/__tests__/c8/rest/parseJobs.spec.ts new file mode 100644 index 00000000..60d4d9e9 --- /dev/null +++ b/src/__tests__/c8/rest/parseJobs.spec.ts @@ -0,0 +1,23 @@ +import { createSpecializedRestApiJobClass } from '../../../c8/lib/RestApiJobClassFactory' +import { Int64String, LosslessDto, losslessParse } from '../../../lib' + +class Variables extends LosslessDto { + @Int64String + bigValue!: string +} + +class CustomHeaders extends LosslessDto { + @Int64String + bigHeader!: string + smallHeader!: number +} + +const myJob = createSpecializedRestApiJobClass(Variables, CustomHeaders) + +test('It correctly parses variables and custom headers', () => { + const jsonString = `{"jobs":[{"key":2251799813737371,"type":"console-log-complete","processInstanceKey":2251799813737366,"bpmnProcessId":"hello-world-complete","processDefinitionVersion":1,"processDefinitionKey":2251799813736299,"elementId":"ServiceTask_0g6tf5f","elementInstanceKey":2251799813737370,"customHeaders":{"message":"Hello World","bigHeader":1,"smallHeader":2},"worker":"test","retries":100,"deadline":1725501895792,"variables":{"bigValue":3},"tenantId":""}]}` + const res = losslessParse(jsonString, myJob, 'jobs') + expect(res[0].variables.bigValue).toBe('3') + expect(res[0].customHeaders.smallHeader).toBe(2) + expect(res[0].customHeaders.bigHeader).toBe('1') +}) diff --git a/src/__tests__/lib/LosslessJsonParser.unit.spec.ts b/src/__tests__/lib/LosslessJsonParser.unit.spec.ts index 26d22244..7d9d5628 100644 --- a/src/__tests__/lib/LosslessJsonParser.unit.spec.ts +++ b/src/__tests__/lib/LosslessJsonParser.unit.spec.ts @@ -344,3 +344,11 @@ test('LosslessStringify correctly handles null objects', () => { const stringifiedDto = losslessStringify(json) expect(stringifiedDto).toBe(`{"abc":[null,null,null]}`) // 3 (string) }) + +test('LosslessJsonParser handles subkeys', () => { + const jsonString = `{"jobs":[{"key":2251799813737371,"type":"console-log-complete","processInstanceKey":2251799813737366,"bpmnProcessId":"hello-world-complete","processDefinitionVersion":1,"processDefinitionKey":2251799813736299,"elementId":"ServiceTask_0g6tf5f","elementInstanceKey":2251799813737370,"customHeaders":{"message":"Hello World"},"worker":"test","retries":100,"deadline":1725501895792,"variables":{},"tenantId":""}]}` + + const parsed = losslessParse(jsonString, undefined, 'jobs') + console.log(parsed) + expect(parsed[0].key).toBe(2251799813737371) +}) diff --git a/src/c8/lib/C8Dto.ts b/src/c8/lib/C8Dto.ts index 3ebfc90d..a60dc653 100644 --- a/src/c8/lib/C8Dto.ts +++ b/src/c8/lib/C8Dto.ts @@ -1,9 +1,11 @@ import { LosslessNumber } from 'lossless-json' import { Int64String, LosslessDto } from '../../lib' -import { JSONDoc } from '../../zeebe/types' -export class Job extends LosslessDto { +export class RestApiJob< + Variables = LosslessDto, + CustomHeaders = LosslessDto, +> extends LosslessDto { @Int64String key!: string type!: string @@ -16,12 +18,12 @@ export class Job extends LosslessDto { elementId!: string @Int64String elementInstanceKey!: LosslessNumber - customHeaders!: T + customHeaders!: CustomHeaders worker!: string retries!: number @Int64String deadline!: LosslessNumber - variables!: JSONDoc + variables!: Variables tenantId!: string } @@ -55,3 +57,6 @@ export interface NewUserInfo { email: string enabled: boolean } + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type Ctor = new (obj: any) => T diff --git a/src/c8/lib/C8RestClient.ts b/src/c8/lib/C8RestClient.ts index a1c73efe..477124a7 100644 --- a/src/c8/lib/C8RestClient.ts +++ b/src/c8/lib/C8RestClient.ts @@ -4,18 +4,18 @@ import got from 'got' import { CamundaEnvironmentConfigurator, CamundaPlatform8Configuration, - DeepPartial, - GetCustomCertificateBuffer, - GotRetryConfig, - LosslessDto, - RequireConfiguration, constructOAuthProvider, createUserAgentString, + DeepPartial, + GetCustomCertificateBuffer, gotBeforeErrorHook, gotErrorHandler, + GotRetryConfig, + LosslessDto, losslessParse, losslessStringify, makeBeforeRetryHandlerFor401TokenRetry, + RequireConfiguration, } from '../../lib' import { IOAuthProvider } from '../../oauth' import { @@ -23,12 +23,17 @@ import { CompleteJobRequest, ErrorJobWithVariables, FailJobRequest, + IProcessVariables, + Job, + JOB_ACTION_ACKNOWLEDGEMENT, + JobCompletionInterfaceRest, PublishMessageRequest, PublishMessageResponse, TopologyResponse, } from '../../zeebe/types' -import { Job, JobUpdateChangeset, NewUserInfo, TaskChangeSet } from './C8Dto' +import { Ctor, JobUpdateChangeset, NewUserInfo, TaskChangeSet } from './C8Dto' +import { createSpecializedRestApiJobClass } from './RestApiJobClassFactory' const trace = debug('camunda:zeebe') @@ -240,42 +245,72 @@ export class C8RestClient { /** * 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. + * The parameter Variables is a Dto to decode the job payload. The CustomHeaders 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 other applications that natively support the int64 type. */ - public async activateJobs( - request: ActivateJobsRequest - ): Promise { + public async activateJobs< + VariablesDto extends LosslessDto, + CustomHeadersDto extends LosslessDto, + >( + request: ActivateJobsRequest & { + inputVariableDto?: Ctor + customHeadersDto?: Ctor + } + ): Promise< + (Job & + JobCompletionInterfaceRest)[] + > { const headers = await this.getHeaders() + const { + inputVariableDto = LosslessDto, + customHeadersDto = LosslessDto, + ...req + } = request + + const body = losslessStringify(this.addDefaultTenantId(req)) + + const jobDto = createSpecializedRestApiJobClass( + inputVariableDto, + customHeadersDto + ) + return this.rest.then((rest) => rest .post(`jobs/activation`, { - body: losslessStringify(this.addDefaultTenantId(request)), + body, headers, - parseJson: (text) => losslessParse(text, Job), + parseJson: (text) => losslessParse(text, jobDto, 'jobs'), }) - .json() + .json<{ jobs: Job[] }>() + .then((activatedJobs) => activatedJobs.jobs.map(this.addJobMethods)) ) } /** * 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. + * @throws + * Documentation: https://docs.camunda.io/docs/next/apis-tools/camunda-api-rest/specifications/fail-job/ */ 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, - }) + rest + .post(`jobs/${jobKey}/fail`, { + body: losslessStringify(failJobRequest), + headers, + }) + .then(() => JOB_ACTION_ACKNOWLEDGEMENT) ) } /** * Reports a business error (i.e. non-technical) that occurs while processing a job. + * @throws + * Documentation: https://docs.camunda.io/docs/next/apis-tools/camunda-api-rest/specifications/report-error-for-job/ */ public async errorJob( errorJobRequest: ErrorJobWithVariables & { jobKey: string } @@ -284,25 +319,31 @@ export class C8RestClient { const headers = await this.getHeaders() return this.rest.then((rest) => - rest.post(`jobs/${jobKey}/error`, { - body: losslessStringify(request), - headers, - }) + rest + .post(`jobs/${jobKey}/error`, { + body: losslessStringify(request), + headers, + }) + .then(() => 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/ + * @throws */ 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, - }) + rest + .post(`jobs/${jobKey}/complete`, { + body: losslessStringify({ variables: completeJobRequest.variables }), + headers, + }) + .then(() => JOB_ACTION_ACKNOWLEDGEMENT) ) } @@ -333,6 +374,31 @@ export class C8RestClient { ) } + private addJobMethods = ( + job: Job + ): Job & + JobCompletionInterfaceRest => { + return { + ...job, + cancelWorkflow: () => { + throw new Error('Not Implemented') + }, + complete: (variables: IProcessVariables = {}) => + this.completeJob({ + jobKey: job.key, + variables, + }), + error: (error) => + this.errorJob({ + ...error, + jobKey: job.key, + }), + fail: (failJobRequest) => this.failJob(failJobRequest), + /* At this point, no capacity handling in the SDK is implemented, so this has no effect */ + forward: () => JOB_ACTION_ACKNOWLEDGEMENT, + } + } + /** * Helper method to add the default tenantIds if we are not passed explicit tenantIds */ diff --git a/src/c8/lib/RestApiJobClassFactory.ts b/src/c8/lib/RestApiJobClassFactory.ts new file mode 100644 index 00000000..fde2a485 --- /dev/null +++ b/src/c8/lib/RestApiJobClassFactory.ts @@ -0,0 +1,67 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { LosslessDto } from '../../lib' + +import { RestApiJob } from './C8Dto' + +const factory = createMemoizedSpecializedRestApiJobClassFactory() + +// Creates a specialized RestApiJob class that is cached based on input variables and custom headers. +export const createSpecializedRestApiJobClass = < + Variables extends LosslessDto, + CustomHeaders extends LosslessDto, +>( + inputVariableDto: new (obj: any) => Variables, + customHeaders: new (obj: any) => CustomHeaders +) => { + // Assuming `createMemoizedSpecializedRestApiJobClassFactory` is available + return factory(inputVariableDto, customHeaders) +} + +function createMemoizedSpecializedRestApiJobClassFactory() { + const cache = new Map() + + return function < + Variables extends LosslessDto, + CustomHeaders extends LosslessDto, + >( + inputVariableDto: new (obj: any) => Variables, + customHeaders: new (obj: any) => CustomHeaders + ): new (obj: any) => RestApiJob { + // Create a unique cache key based on the class and inputs + const cacheKey = JSON.stringify({ + inputVariableDto, + customHeaders, + }) + + // Check for cached result + if (cache.has(cacheKey)) { + return cache.get(cacheKey) + } + + // Create a new class that extends the original class + class NewRestApiJobClass< + Variables extends LosslessDto, + CustomHeaders extends LosslessDto, + > extends RestApiJob {} + + // Use Reflect to define the metadata on the new class's prototype + Reflect.defineMetadata( + 'child:class', + inputVariableDto, + NewRestApiJobClass.prototype, + 'variables' + ) + Reflect.defineMetadata( + 'child:class', + customHeaders, + NewRestApiJobClass.prototype, + 'customHeaders' + ) + + // Store the new class in cache + cache.set(cacheKey, NewRestApiJobClass) + + // Return the new class + return NewRestApiJobClass + } +} diff --git a/src/lib/LosslessJsonParser.ts b/src/lib/LosslessJsonParser.ts index 8c62a54d..dad06c5c 100644 --- a/src/lib/LosslessJsonParser.ts +++ b/src/lib/LosslessJsonParser.ts @@ -134,10 +134,16 @@ export function losslessParseArray( */ export function losslessParse( json: string, - dto?: { new (...args: any[]): T } + dto?: { new (...args: any[]): T }, + keyToParse?: string ): T { const parsedLossless = parse(json) as any + // If keyToParse is provided, check for it in the parsed object + if (keyToParse && parsedLossless[keyToParse]) { + return losslessParse(stringify(parsedLossless[keyToParse]) as string, dto) + } + if (Array.isArray(parsedLossless)) { debug(`Array input detected. Parsing array.`) return parseArrayWithAnnotations( diff --git a/src/lib/test.ts b/src/lib/test.ts new file mode 100644 index 00000000..4e9c93b4 --- /dev/null +++ b/src/lib/test.ts @@ -0,0 +1,54 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Int64String, LosslessDto, losslessParse } from './LosslessJsonParser' + +class JobDto extends LosslessDto { + @Int64String + public key!: string + variables!: Variables + customHeaders!: CustomHeaders +} + +class Variables extends LosslessDto { + @Int64String + bigValue!: string +} + +class CustomHeaders extends LosslessDto { + @Int64String + bigHeader!: string + smallHeader!: number +} + +function extendClass( + jobClass: typeof JobDto, + variables: T, + customHeaders: V +) { + Reflect.defineMetadata( + 'child:class', + variables, + jobClass.prototype, + 'variables' + ) + Reflect.defineMetadata( + 'child:class', + customHeaders, + JobDto.prototype, + 'customHeaders' + ) + return jobClass +} + +extendClass(JobDto, Variables, CustomHeaders) + +console.log(Reflect.hasMetadata('Int64String', JobDto, 'key')) // true +const res = losslessParse( + `{"key":123,"variables":{"bigValue": 45},"customHeaders":{"bigHeader":69, "smallHeader": 23}}`, + JobDto +) + +console.log(res) +console.log(typeof res.key) // should be string +console.log(typeof res.variables.bigValue) // should be string +console.log(typeof res.customHeaders.bigHeader) // should be string +console.log(typeof res.customHeaders.smallHeader) // should be number diff --git a/src/zeebe/lib/interfaces-1.0.ts b/src/zeebe/lib/interfaces-1.0.ts index 38b71ce1..e431f883 100644 --- a/src/zeebe/lib/interfaces-1.0.ts +++ b/src/zeebe/lib/interfaces-1.0.ts @@ -199,6 +199,44 @@ export interface JobCompletionInterface { error: ErrorJobOutcome } +export interface JobCompletionInterfaceRest { + /** + * Cancel the workflow. + */ + cancelWorkflow: () => Promise + /** + * Complete the job with a success, optionally passing in a state update to merge + * with the process variables on the broker. + */ + complete: ( + updatedVariables?: WorkerOutputVariables + ) => Promise + /** + * Fail the job with an informative message as to the cause. Optionally, pass in a + * value remaining retries. If no value is passed for retries then the current retry + * count is decremented. Pass in `0`for retries to raise an incident in Operate. Optionally, + * specify a retry backoff period in milliseconds. Default is 0ms (immediate retry) if not + * specified. + */ + fail: typeof FailureHandler + /** + * Mark this job as forwarded to another system for completion. No action is taken by the broker. + * This method releases worker capacity to handle another job. + */ + forward: () => JOB_ACTION_ACKNOWLEDGEMENT + /** + * + * Report a business error (i.e. non-technical) that occurs while processing a job. + * The error is handled in the process by an error catch event. + * If there is no error catch event with the specified errorCode then an incident will be raised instead. + */ + error: (error: ErrorJobWithVariables) => Promise + /** + * Extend the timeout for the job - to be implemented when ModifyJobTimeout becomes available + */ + // extendJobTimeout() +} + export interface ZeebeJob< WorkerInputVariables = IInputVariables, CustomHeaderShape = ICustomHeaders, From ec1654398cb81516262aa86c3073e239b73fd7da Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Fri, 6 Sep 2024 13:29:39 +1200 Subject: [PATCH 04/34] chore(repo): remove extra file --- src/c8/lib/RestApiJobClassFactory.ts | 6 ++-- src/lib/test.ts | 54 ---------------------------- 2 files changed, 3 insertions(+), 57 deletions(-) delete mode 100644 src/lib/test.ts diff --git a/src/c8/lib/RestApiJobClassFactory.ts b/src/c8/lib/RestApiJobClassFactory.ts index fde2a485..b1306c54 100644 --- a/src/c8/lib/RestApiJobClassFactory.ts +++ b/src/c8/lib/RestApiJobClassFactory.ts @@ -25,12 +25,12 @@ function createMemoizedSpecializedRestApiJobClassFactory() { CustomHeaders extends LosslessDto, >( inputVariableDto: new (obj: any) => Variables, - customHeaders: new (obj: any) => CustomHeaders + customHeadersDto: new (obj: any) => CustomHeaders ): new (obj: any) => RestApiJob { // Create a unique cache key based on the class and inputs const cacheKey = JSON.stringify({ inputVariableDto, - customHeaders, + customHeadersDto, }) // Check for cached result @@ -53,7 +53,7 @@ function createMemoizedSpecializedRestApiJobClassFactory() { ) Reflect.defineMetadata( 'child:class', - customHeaders, + customHeadersDto, NewRestApiJobClass.prototype, 'customHeaders' ) diff --git a/src/lib/test.ts b/src/lib/test.ts deleted file mode 100644 index 4e9c93b4..00000000 --- a/src/lib/test.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { Int64String, LosslessDto, losslessParse } from './LosslessJsonParser' - -class JobDto extends LosslessDto { - @Int64String - public key!: string - variables!: Variables - customHeaders!: CustomHeaders -} - -class Variables extends LosslessDto { - @Int64String - bigValue!: string -} - -class CustomHeaders extends LosslessDto { - @Int64String - bigHeader!: string - smallHeader!: number -} - -function extendClass( - jobClass: typeof JobDto, - variables: T, - customHeaders: V -) { - Reflect.defineMetadata( - 'child:class', - variables, - jobClass.prototype, - 'variables' - ) - Reflect.defineMetadata( - 'child:class', - customHeaders, - JobDto.prototype, - 'customHeaders' - ) - return jobClass -} - -extendClass(JobDto, Variables, CustomHeaders) - -console.log(Reflect.hasMetadata('Int64String', JobDto, 'key')) // true -const res = losslessParse( - `{"key":123,"variables":{"bigValue": 45},"customHeaders":{"bigHeader":69, "smallHeader": 23}}`, - JobDto -) - -console.log(res) -console.log(typeof res.key) // should be string -console.log(typeof res.variables.bigValue) // should be string -console.log(typeof res.customHeaders.bigHeader) // should be string -console.log(typeof res.customHeaders.smallHeader) // should be number From a49d217f95b9b550bedd9af79fe9d950ad31add2 Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Mon, 9 Sep 2024 16:39:06 +1200 Subject: [PATCH 05/34] feat(zeebe): create and cancel process instances over REST --- README.md | 2 +- src/c8/lib/C8RestClient.ts | 55 +++++++++++++++++++++++++++++++-- src/lib/LosslessJsonParser.ts | 5 +++ src/zeebe/lib/interfaces-1.0.ts | 9 ++++-- 4 files changed, 65 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 9de1fa17..703e4ba7 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ Entity keys in Camunda 8 are stored and represented as `int64` numbers. The rang Some number values - for example: "_total returned results_ " - may be specified as `int64` in the API specifications. Although these numbers will usually not contain unsafe values, they are always serialised to `string`. -For `int64` values whose type is not known ahead of time, such as job variables, you can pass an annotated data transfer object (DTO) to decode them reliably. If no DTO is specified, the default behavior of the SDK is to serialise all numbers to JavaScript `number`, and if a number value is detected at a runtime that cannot be accurately stored as `number`, to throw an exception. +For `int64` values whose type is not known ahead of time, such as job variables, you can pass an annotated data transfer object (DTO) to decode them reliably. If no DTO is specified, the default behavior of the SDK is to serialise all numbers to JavaScript `number`, and to throw an exception if a number value is detected at a runtime that cannot be accurately represented as the JavaScript `number` type (that is, a value greater than 2^53-1). ## Authorization diff --git a/src/c8/lib/C8RestClient.ts b/src/c8/lib/C8RestClient.ts index 477124a7..edd34129 100644 --- a/src/c8/lib/C8RestClient.ts +++ b/src/c8/lib/C8RestClient.ts @@ -21,6 +21,7 @@ import { IOAuthProvider } from '../../oauth' import { ActivateJobsRequest, CompleteJobRequest, + CreateProcessInstanceRequest, ErrorJobWithVariables, FailJobRequest, IProcessVariables, @@ -58,6 +59,7 @@ export class C8RestClient { options?.oAuthProvider ?? constructOAuthProvider(config) this.userAgentString = createUserAgentString(config) this.tenantId = config.CAMUNDA_TENANT_ID + const baseUrl = RequireConfiguration( config.ZEEBE_REST_ADDRESS, 'ZEEBE_REST_ADDRESS' @@ -269,7 +271,7 @@ export class C8RestClient { ...req } = request - const body = losslessStringify(this.addDefaultTenantId(req)) + const body = losslessStringify(this.addDefaultTenantIds(req)) const jobDto = createSpecializedRestApiJobClass( inputVariableDto, @@ -364,6 +366,9 @@ export class C8RestClient { ) } + /** + * 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. + */ public async resolveIncident(incidentKey: string) { const headers = await this.getHeaders() @@ -374,6 +379,40 @@ export class C8RestClient { ) } + /** + * Create and start a process instance + */ + public async createProcessInstance(request: CreateProcessInstanceRequest) { + const headers = await this.getHeaders() + + return this.rest.then((rest) => + rest.post(`process-instances`, { + body: losslessStringify(this.addDefaultTenantId(request)), + headers, + }) + ) + } + + /** + * Cancel an active process instance + */ + public async cancelProcessInstance({ + processInstanceKey, + operationReference, + }: { + processInstanceKey: string + operationReference?: string + }) { + const headers = await this.getHeaders() + + return this.rest.then((rest) => + rest.post(`process-instances/${processInstanceKey}/cancellation`, { + body: JSON.stringify({ operationReference }), + headers, + }) + ) + } + private addJobMethods = ( job: Job ): Job & @@ -396,14 +435,24 @@ export class C8RestClient { fail: (failJobRequest) => this.failJob(failJobRequest), /* At this point, no capacity handling in the SDK is implemented, so this has no effect */ forward: () => JOB_ACTION_ACKNOWLEDGEMENT, + modifyJobTimeout: ({ newTimeoutMs }: { newTimeoutMs: number }) => + this.updateJob({ jobKey: job.key, timeout: newTimeoutMs }), } } /** * 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] : [] + private addDefaultTenantId(request: T) { + const tenantId = request.tenantId ?? this.tenantId + return { ...request, tenantId } + } + + /** + * Helper method to add the default tenantIds if we are not passed explicit tenantIds + */ + private addDefaultTenantIds(request: T) { + const tenantIds = request.tenantIds ?? [this.tenantId] return { ...request, tenantIds } } } diff --git a/src/lib/LosslessJsonParser.ts b/src/lib/LosslessJsonParser.ts index dad06c5c..e2bf24df 100644 --- a/src/lib/LosslessJsonParser.ts +++ b/src/lib/LosslessJsonParser.ts @@ -7,7 +7,11 @@ * * It also handles nested Dtos by using the `@ChildDto` decorator. * + * Update: added an optional `key` parameter to support the Camunda 8 REST API's use of an array under a key, e.g. { jobs : Job[] } + * * More details on the design here: https://github.com/camunda/camunda-8-js-sdk/issues/81#issuecomment-2022213859 + * + * See this article to understand why this is necessary: https://jsoneditoronline.org/indepth/parse/why-does-json-parse-corrupt-large-numbers/ */ /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -127,6 +131,7 @@ export function losslessParseArray( } /** + * losslessParse uses lossless-json parse to deserialize JSON. * With no Dto, the parser will throw if it encounters an int64 number that cannot be safely represented as a JS number. * * @param json the JSON string to parse diff --git a/src/zeebe/lib/interfaces-1.0.ts b/src/zeebe/lib/interfaces-1.0.ts index e431f883..80079ff2 100644 --- a/src/zeebe/lib/interfaces-1.0.ts +++ b/src/zeebe/lib/interfaces-1.0.ts @@ -1,5 +1,6 @@ import { ClientReadableStream } from '@grpc/grpc-js' import { Chalk } from 'chalk' +import { Response } from 'got' import { LosslessNumber } from 'lossless-json' import { MaybeTimeDuration } from 'typed-duration' @@ -232,9 +233,13 @@ export interface JobCompletionInterfaceRest { */ error: (error: ErrorJobWithVariables) => Promise /** - * Extend the timeout for the job - to be implemented when ModifyJobTimeout becomes available + * Extend the timeout for the job by setting a new timeout */ - // extendJobTimeout() + modifyJobTimeout: ({ + newTimeoutMs, + }: { + newTimeoutMs: number + }) => Promise> } export interface ZeebeJob< From cd6080fcaa8073e4655f72127af30db11f9ef743 Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Fri, 13 Sep 2024 12:30:16 +1200 Subject: [PATCH 06/34] fix(zeebe): do not override explicit ZEEBE_GRPC_ADDRESS with default ZEEBE_ADDRESS fixes #245 --- src/lib/Configuration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/Configuration.ts b/src/lib/Configuration.ts index 6ec78da1..9d56b592 100644 --- a/src/lib/Configuration.ts +++ b/src/lib/Configuration.ts @@ -24,6 +24,7 @@ const getMainEnv = () => ZEEBE_GRPC_ADDRESS: { type: 'string', optional: true, + default: 'localhost:26500', }, /** The address for the Zeebe REST API. Defaults to localhost:8080 */ ZEEBE_REST_ADDRESS: { @@ -35,7 +36,6 @@ const getMainEnv = () => ZEEBE_ADDRESS: { type: 'string', optional: true, - default: 'localhost:26500', }, /** This is the client ID for the client credentials */ ZEEBE_CLIENT_ID: { From 40a63164a7588ea81fa0df16e9538fa5366dc049 Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Tue, 17 Sep 2024 12:27:23 +1200 Subject: [PATCH 07/34] fix(zeebe): throw on client if array passed as variables to CompleteJob fixes #247 --- .../zeebe/stringifyVariables.unit.spec.ts | 6 ++++++ src/zeebe/lib/stringifyVariables.ts | 20 +++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/__tests__/zeebe/stringifyVariables.unit.spec.ts b/src/__tests__/zeebe/stringifyVariables.unit.spec.ts index 4b493e0f..5b658d78 100644 --- a/src/__tests__/zeebe/stringifyVariables.unit.spec.ts +++ b/src/__tests__/zeebe/stringifyVariables.unit.spec.ts @@ -50,6 +50,12 @@ test('stringifyVariables stringifies the variables key of a job object', () => { expect(stringified.variables).toBe(expectedStringifiedVariables) }) +test('stringifyVariables throws an error when passed an array', () => { + const arrayInput = { variables: ['something'] } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(() => stringifyVariables(arrayInput as any)).toThrow(Error) +}) + test('parseVariables returns a new object', () => { expect(parseVariables(jobDictionary)).not.toEqual(jobDictionary) }) diff --git a/src/zeebe/lib/stringifyVariables.ts b/src/zeebe/lib/stringifyVariables.ts index 57770490..216102a9 100644 --- a/src/zeebe/lib/stringifyVariables.ts +++ b/src/zeebe/lib/stringifyVariables.ts @@ -11,11 +11,14 @@ export function parseVariables( }) } +/** + * Parse an incoming job and convert its variables and custom headers to JSON. + */ + export function parseVariablesAndCustomHeadersToJSON( response: ActivatedJob, - // eslint-disable-next-line @typescript-eslint/no-explicit-any + /* eslint-disable @typescript-eslint/no-explicit-any */ inputVariableDto: new (...args: any[]) => Readonly, - // eslint-disable-next-line @typescript-eslint/no-explicit-any customHeadersDto: new (...args: any[]) => Readonly ): Promise> { return new Promise((resolve, reject) => { @@ -40,12 +43,25 @@ export function parseVariablesAndCustomHeadersToJSON( }) } +/** + * Turn the `variables` field of a request from a JS object to a JSON string + * This should be a key:value object where the keys will be variable names in Zeebe and the values are the corresponding values. + * This function is used when sending a job back to Zeebe. + */ export function stringifyVariables< K, T extends { variables: K extends JSONDoc ? K : K }, V extends T & { variables: string }, >(request: T): V { const variables = request.variables || {} + /** + * This is a run-time guard. The type system disallows passing an array, but type erasure and dynamic programming can override that. + * If you pass an array as the variables to a CompleteJob RPC call, it will report success, but fail on the broker, stalling the process. + * See: https://github.com/camunda/camunda-8-js-sdk/issues/247 + */ + if (Array.isArray(variables)) { + throw new Error('Unable to parse Array into variables') + } const variablesString = losslessStringify(variables) return Object.assign({}, request, { variables: variablesString }) as V } From debd2122e713e98e3180a0bf0b200a1560788b81 Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Tue, 17 Sep 2024 22:41:05 +1200 Subject: [PATCH 08/34] feat(camunda8): implement deployResources REST API --- package-lock.json | 523 ++++++++++++------ package.json | 3 +- src/__tests__/c8/rest/activateJobs.spec.ts | 23 +- src/__tests__/config/jest.cleanup.ts | 1 - .../lib/LosslessJsonParser.unit.spec.ts | 32 ++ .../integration/Client-integration.spec.ts | 2 +- src/c8/lib/C8Dto.ts | 10 +- src/c8/lib/C8RestClient.ts | 72 ++- src/lib/LosslessJsonParser.ts | 67 ++- src/zeebe/lib/GrpcClient.ts | 5 +- src/zeebe/lib/interfaces-grpc-1.0.ts | 2 +- 11 files changed, 508 insertions(+), 232 deletions(-) diff --git a/package-lock.json b/package-lock.json index a7959ff1..70651311 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "dayjs": "^1.8.15", "debug": "^4.3.4", "fast-xml-parser": "^4.1.3", + "form-data": "^4.0.0", "got": "^11.8.6", "jwt-decode": "^4.0.0", "lodash.mergewith": "^4.6.2", @@ -4501,7 +4502,6 @@ }, "node_modules/asynckit": { "version": "0.4.0", - "dev": true, "license": "MIT" }, "node_modules/at-least-node": { @@ -4761,9 +4761,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dev": true, "dependencies": { "bytes": "3.1.2", @@ -4774,7 +4774,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -5006,13 +5006,19 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dev": true, - "license": "MIT", "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5188,8 +5194,9 @@ }, "node_modules/cli-truncate": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", "dev": true, - "license": "MIT", "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" @@ -5202,9 +5209,10 @@ } }, "node_modules/cli-truncate/node_modules/ansi-regex": { - "version": "6.0.1", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -5213,14 +5221,16 @@ } }, "node_modules/cli-truncate/node_modules/emoji-regex": { - "version": "10.3.0", - "dev": true, - "license": "MIT" + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true }, "node_modules/cli-truncate/node_modules/string-width": { - "version": "7.0.0", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, - "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", @@ -5235,8 +5245,9 @@ }, "node_modules/cli-truncate/node_modules/strip-ansi": { "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, - "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -5321,12 +5332,12 @@ }, "node_modules/colorette": { "version": "2.0.20", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true }, "node_modules/combined-stream": { "version": "1.0.8", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -5336,11 +5347,12 @@ } }, "node_modules/commander": { - "version": "11.1.0", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "dev": true, - "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/commitizen": { @@ -6224,16 +6236,20 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, - "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-properties": { @@ -6254,7 +6270,6 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -6546,9 +6561,9 @@ "license": "MIT" }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, "engines": { "node": ">= 0.8" @@ -6697,6 +6712,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/err-code": { "version": "1.1.2", "license": "MIT" @@ -6761,6 +6788,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-set-tostringtag": { "version": "2.0.2", "dev": true, @@ -7431,6 +7479,12 @@ "node": ">= 0.6" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, "node_modules/execa": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/execa/-/execa-3.4.0.tgz", @@ -7486,37 +7540,37 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "dev": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -7622,7 +7676,9 @@ "license": "MIT" }, "node_modules/fast-xml-parser": { - "version": "4.3.2", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz", + "integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==", "funding": [ { "type": "github", @@ -7633,7 +7689,6 @@ "url": "https://paypal.me/naturalintelligence" } ], - "license": "MIT", "dependencies": { "strnum": "^1.0.5" }, @@ -7695,13 +7750,13 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dev": true, "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -7822,7 +7877,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -7994,8 +8048,9 @@ }, "node_modules/get-east-asian-width": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", + "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", "dev": true, - "license": "MIT", "engines": { "node": ">=18" }, @@ -8004,15 +8059,20 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dev": true, - "license": "MIT", "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8312,11 +8372,12 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, - "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10922,29 +10983,33 @@ } }, "node_modules/lilconfig": { - "version": "3.0.0", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", "dev": true, - "license": "MIT", "engines": { "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/lint-staged": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.2.tgz", - "integrity": "sha512-TiTt93OPh1OZOsb5B7k96A/ATl2AjIZo+vnzFZ6oHK5FuTk63ByDtxGQpHm+kFETjEWqgkF95M8FRXKR/LEBcw==", - "dev": true, - "dependencies": { - "chalk": "5.3.0", - "commander": "11.1.0", - "debug": "4.3.4", - "execa": "8.0.1", - "lilconfig": "3.0.0", - "listr2": "8.0.1", - "micromatch": "4.0.5", - "pidtree": "0.6.0", - "string-argv": "0.3.2", - "yaml": "2.3.4" + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.10.tgz", + "integrity": "sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg==", + "dev": true, + "dependencies": { + "chalk": "~5.3.0", + "commander": "~12.1.0", + "debug": "~4.3.6", + "execa": "~8.0.1", + "lilconfig": "~3.1.2", + "listr2": "~8.2.4", + "micromatch": "~4.0.8", + "pidtree": "~0.6.0", + "string-argv": "~0.3.2", + "yaml": "~2.5.0" }, "bin": { "lint-staged": "bin/lint-staged.js" @@ -10968,6 +11033,23 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/lint-staged/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/lint-staged/node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -11036,6 +11118,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lint-staged/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, "node_modules/lint-staged/node_modules/npm-run-path": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", @@ -11102,26 +11190,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lint-staged/node_modules/yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/listr2": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.0.1.tgz", - "integrity": "sha512-ovJXBXkKGfq+CwmKTjluEqFi3p4h8xvkxGQQAQan22YCgef4KZ1mKGjzfGh6PL6AW5Csw0QiQPNuQyH+6Xk3hA==", + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.4.tgz", + "integrity": "sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==", "dev": true, "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", - "log-update": "^6.0.0", - "rfdc": "^1.3.0", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" }, "engines": { @@ -11129,9 +11208,10 @@ } }, "node_modules/listr2/node_modules/ansi-regex": { - "version": "6.0.1", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -11141,8 +11221,9 @@ }, "node_modules/listr2/node_modules/ansi-styles": { "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -11151,19 +11232,16 @@ } }, "node_modules/listr2/node_modules/emoji-regex": { - "version": "10.3.0", - "dev": true, - "license": "MIT" - }, - "node_modules/listr2/node_modules/eventemitter3": { - "version": "5.0.1", - "dev": true, - "license": "MIT" + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true }, "node_modules/listr2/node_modules/string-width": { - "version": "7.0.0", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, - "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", @@ -11178,8 +11256,9 @@ }, "node_modules/listr2/node_modules/strip-ansi": { "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, - "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -11192,8 +11271,9 @@ }, "node_modules/listr2/node_modules/wrap-ansi": { "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", @@ -11420,13 +11500,14 @@ } }, "node_modules/log-update": { - "version": "6.0.0", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", "dev": true, - "license": "MIT", "dependencies": { - "ansi-escapes": "^6.2.0", - "cli-cursor": "^4.0.0", - "slice-ansi": "^7.0.0", + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" }, @@ -11438,23 +11519,25 @@ } }, "node_modules/log-update/node_modules/ansi-escapes": { - "version": "6.2.0", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", "dev": true, - "license": "MIT", "dependencies": { - "type-fest": "^3.0.0" + "environment": "^1.0.0" }, "engines": { - "node": ">=14.16" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/log-update/node_modules/ansi-regex": { - "version": "6.0.1", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -11464,8 +11547,9 @@ }, "node_modules/log-update/node_modules/ansi-styles": { "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -11474,28 +11558,31 @@ } }, "node_modules/log-update/node_modules/cli-cursor": { - "version": "4.0.0", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, - "license": "MIT", "dependencies": { - "restore-cursor": "^4.0.0" + "restore-cursor": "^5.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/log-update/node_modules/emoji-regex": { - "version": "10.3.0", - "dev": true, - "license": "MIT" + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true }, "node_modules/log-update/node_modules/is-fullwidth-code-point": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", "dev": true, - "license": "MIT", "dependencies": { "get-east-asian-width": "^1.0.0" }, @@ -11506,25 +11593,54 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/log-update/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/log-update/node_modules/restore-cursor": { - "version": "4.0.0", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, - "license": "MIT", "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/log-update/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/log-update/node_modules/slice-ansi": { "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" @@ -11537,9 +11653,10 @@ } }, "node_modules/log-update/node_modules/string-width": { - "version": "7.0.0", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, - "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", @@ -11554,8 +11671,9 @@ }, "node_modules/log-update/node_modules/strip-ansi": { "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, - "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -11566,21 +11684,11 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/log-update/node_modules/type-fest": { - "version": "3.13.1", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/log-update/node_modules/wrap-ansi": { "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", @@ -11846,10 +11954,13 @@ "license": "MIT" }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", - "dev": true + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -11874,11 +11985,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, - "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -11901,7 +12013,6 @@ }, "node_modules/mime-db": { "version": "1.52.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -11909,7 +12020,6 @@ }, "node_modules/mime-types": { "version": "2.1.35", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -11926,6 +12036,18 @@ "node": ">=6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mimic-response": { "version": "1.0.1", "license": "MIT", @@ -15308,9 +15430,9 @@ "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", "dev": true }, "node_modules/path-type": { @@ -15659,12 +15781,12 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dev": true, "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -16033,9 +16155,10 @@ } }, "node_modules/rfdc": { - "version": "1.3.0", - "dev": true, - "license": "MIT" + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true }, "node_modules/rimraf": { "version": "3.0.2", @@ -16653,9 +16776,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dev": true, "dependencies": { "debug": "2.6.9", @@ -16691,6 +16814,15 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/send/node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -16710,15 +16842,15 @@ "dev": true }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dev": true, "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" @@ -16731,14 +16863,17 @@ "dev": true }, "node_modules/set-function-length": { - "version": "1.1.1", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, - "license": "MIT", "dependencies": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -16828,13 +16963,18 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -16895,8 +17035,9 @@ }, "node_modules/slice-ansi": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" @@ -16910,8 +17051,9 @@ }, "node_modules/slice-ansi/node_modules/ansi-styles": { "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -16921,8 +17063,9 @@ }, "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -18479,6 +18622,18 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", + "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/yargs": { "version": "17.7.2", "license": "MIT", diff --git a/package.json b/package.json index df204ecf..882853dd 100644 --- a/package.json +++ b/package.json @@ -147,6 +147,7 @@ "dayjs": "^1.8.15", "debug": "^4.3.4", "fast-xml-parser": "^4.1.3", + "form-data": "^4.0.0", "got": "^11.8.6", "jwt-decode": "^4.0.0", "lodash.mergewith": "^4.6.2", @@ -159,4 +160,4 @@ "typed-duration": "^1.0.12", "uuid": "^7.0.3" } -} \ No newline at end of file +} diff --git a/src/__tests__/c8/rest/activateJobs.spec.ts b/src/__tests__/c8/rest/activateJobs.spec.ts index 4d9d5050..88e4bba4 100644 --- a/src/__tests__/c8/rest/activateJobs.spec.ts +++ b/src/__tests__/c8/rest/activateJobs.spec.ts @@ -14,9 +14,13 @@ const grpcClient = new ZeebeGrpcClient({ const restClient = new C8RestClient() beforeAll(async () => { - res = await grpcClient.deployResource({ - processFilename: './src/__tests__/testdata/hello-world-complete.bpmn', - }) + res = (await restClient.deployResourcesFromFiles([ + './src/__tests__/testdata/hello-world-complete.bpmn', + ])) as unknown as DeployResourceResponse + // res = await grpcClient.deployResource({ + // processFilename: './src/__tests__/testdata/hello-world-complete.bpmn', + // }) + console.log(res) bpmnProcessId = res.deployments[0].process.bpmnProcessId }) @@ -29,10 +33,11 @@ test('Can service a task', (done) => { grpcClient .createProcessInstance({ bpmnProcessId, - variables: {}, + variables: { + someNumberField: 8, + }, }) - .then((r) => { - console.log(r) + .then(() => { restClient .activateJobs({ maxJobsToActivate: 2, @@ -43,10 +48,8 @@ test('Can service a task', (done) => { }) .then((jobs) => { expect(jobs.length).toBe(1) - console.log(jobs) - const res = jobs.map((job) => job.complete()) - console.log(res) - Promise.all(res).then(() => done()) + + jobs[0].complete().then(() => done()) }) }) }) diff --git a/src/__tests__/config/jest.cleanup.ts b/src/__tests__/config/jest.cleanup.ts index 80bdccf1..2bdfb119 100644 --- a/src/__tests__/config/jest.cleanup.ts +++ b/src/__tests__/config/jest.cleanup.ts @@ -67,7 +67,6 @@ export const cleanUp = async () => { await zeebe.cancelProcessInstance(key) console.log(`Cancelled process instance ${key}`) } catch (e) { - console.log(e) if (!(e as Error).message.startsWith('5 NOT_FOUND')) { console.log('Failed to cancel process instance', key) console.log((e as Error).message) diff --git a/src/__tests__/lib/LosslessJsonParser.unit.spec.ts b/src/__tests__/lib/LosslessJsonParser.unit.spec.ts index 7d9d5628..253d3eb1 100644 --- a/src/__tests__/lib/LosslessJsonParser.unit.spec.ts +++ b/src/__tests__/lib/LosslessJsonParser.unit.spec.ts @@ -352,3 +352,35 @@ test('LosslessJsonParser handles subkeys', () => { console.log(parsed) expect(parsed[0].key).toBe(2251799813737371) }) + +test('LosslessJsonParser will throw if given stringified JSON with an unsafe integer number', () => { + let threw = false + const json = `{"unsafeNumber": 9223372036854775808}` // Unsafe integer (greater than Int64 max) + + try { + losslessParse(json) // Attempt to parse un-mapped JSON directly + } catch (e) { + threw = true + expect((e as Error).message.includes('unsafe number value')).toBe(true) + } + + expect(threw).toBe(true) +}) + +test('LosslessJsonParser will throw if given stringified JSON with an unsafe integer number, even with a Dto', () => { + let threw = false + const json = `{"unsafeNumber": 9223372036854775808}` // Unsafe integer (greater than Int64 max) + + class Dto extends LosslessDto { + unsafeNumber!: number + } + + try { + losslessParse(json, Dto) // Attempt to parse mapped JSON without a mapping + } catch (e) { + threw = true + expect((e as Error).message.includes('unsafe number value')).toBe(true) + } + + expect(threw).toBe(true) +}) diff --git a/src/__tests__/zeebe/integration/Client-integration.spec.ts b/src/__tests__/zeebe/integration/Client-integration.spec.ts index 7770b52c..2b6b5963 100644 --- a/src/__tests__/zeebe/integration/Client-integration.spec.ts +++ b/src/__tests__/zeebe/integration/Client-integration.spec.ts @@ -70,7 +70,7 @@ test("does not retry to cancel a process instance that doesn't exist", async () // See: https://github.com/zeebe-io/zeebe/issues/2680 // await zbc.cancelProcessInstance('123LoL') try { - await zbc.cancelProcessInstance(2251799813686202) + await zbc.cancelProcessInstance('2251799813686202') } catch (e: unknown) { expect((e as Error).message.indexOf('5 NOT_FOUND:')).toBe(0) } diff --git a/src/c8/lib/C8Dto.ts b/src/c8/lib/C8Dto.ts index a60dc653..218c823d 100644 --- a/src/c8/lib/C8Dto.ts +++ b/src/c8/lib/C8Dto.ts @@ -1,5 +1,3 @@ -import { LosslessNumber } from 'lossless-json' - import { Int64String, LosslessDto } from '../../lib' export class RestApiJob< @@ -10,19 +8,19 @@ export class RestApiJob< key!: string type!: string @Int64String - processInstanceKey!: LosslessNumber + processInstanceKey!: string bpmnProcessId!: string processDefinitionVersion!: number @Int64String - processDefinitionKey!: LosslessNumber + processDefinitionKey!: string elementId!: string @Int64String - elementInstanceKey!: LosslessNumber + elementInstanceKey!: string customHeaders!: CustomHeaders worker!: string retries!: number @Int64String - deadline!: LosslessNumber + deadline!: string variables!: Variables tenantId!: string } diff --git a/src/c8/lib/C8RestClient.ts b/src/c8/lib/C8RestClient.ts index edd34129..968a2851 100644 --- a/src/c8/lib/C8RestClient.ts +++ b/src/c8/lib/C8RestClient.ts @@ -1,4 +1,7 @@ +import fs from 'node:fs' + import { debug } from 'debug' +import FormData from 'form-data' import got from 'got' import { @@ -86,8 +89,6 @@ export class C8RestClient { }, }) ) - - // this.tenantId = config.CAMUNDA_TENANT_ID } private async getHeaders() { @@ -285,8 +286,8 @@ export class C8RestClient { headers, parseJson: (text) => losslessParse(text, jobDto, 'jobs'), }) - .json<{ jobs: Job[] }>() - .then((activatedJobs) => activatedJobs.jobs.map(this.addJobMethods)) + .json[]>() + .then((activatedJobs) => activatedJobs.map(this.addJobMethods)) ) } @@ -301,7 +302,7 @@ export class C8RestClient { return this.rest.then((rest) => rest - .post(`jobs/${jobKey}/fail`, { + .post(`jobs/${jobKey}/failure`, { body: losslessStringify(failJobRequest), headers, }) @@ -325,6 +326,7 @@ export class C8RestClient { .post(`jobs/${jobKey}/error`, { body: losslessStringify(request), headers, + parseJson: (text) => losslessParse(text), }) .then(() => JOB_ACTION_ACKNOWLEDGEMENT) ) @@ -341,7 +343,7 @@ export class C8RestClient { return this.rest.then((rest) => rest - .post(`jobs/${jobKey}/complete`, { + .post(`jobs/${jobKey}/completion`, { body: losslessStringify({ variables: completeJobRequest.variables }), headers, }) @@ -367,13 +369,13 @@ export class C8RestClient { } /** - * 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. + * 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. */ public async resolveIncident(incidentKey: string) { const headers = await this.getHeaders() return this.rest.then((rest) => - rest.post(`incidents/${incidentKey}/resolve`, { + rest.post(`incidents/${incidentKey}/resolution`, { headers, }) ) @@ -413,6 +415,60 @@ export class C8RestClient { ) } + /** + * Deploy resources to the broker + */ + /** + * Deploy resources to the broker. + * @param resources - An array of binary data buffers representing the resources to deploy. + * @param tenantId - Optional tenant ID to deploy the resources to. If not provided, the default tenant ID is used. + */ + public async deployResources( + resources: { content: string; name: string }[], + tenantId?: string + ) { + const headers = await this.getHeaders() + const formData = new FormData() + + resources.forEach((resource) => { + formData.append(`resources`, resource.content, { + filename: resource.name, + }) + }) + + if (tenantId || this.tenantId) { + formData.append('tenantId', tenantId ?? this.tenantId) + } + + return this.rest.then((rest) => + rest + .post('deployments', { + body: formData, + headers: { + ...headers, + ...formData.getHeaders(), + Accept: 'application/json', + }, + parseJson: (text) => losslessParse(text), + }) + .json() + ) + } + + public async deployResourcesFromFiles(filenames: string[]) { + const resources: { content: string; name: string }[] = [] + + for (const filename of filenames) { + // const resource = await fs.promises.readFile(filename) + resources.push({ + content: fs.readFileSync(filename, { encoding: 'binary' }), + name: filename, + }) + } + + return this.deployResources(resources) + } + private addJobMethods = ( job: Job ): Job & diff --git a/src/lib/LosslessJsonParser.ts b/src/lib/LosslessJsonParser.ts index e2bf24df..c4d56c7c 100644 --- a/src/lib/LosslessJsonParser.ts +++ b/src/lib/LosslessJsonParser.ts @@ -1,6 +1,9 @@ /** * This is a custom JSON Parser that handles lossless parsing of int64 numbers by using the lossless-json library. * + * This is motivated by the use of int64 for Camunda 8 Entity keys, which are not supported by JavaScript's Number type. + * Variables could also contain unsafe large integers if an external system sends them to the broker. + * * It converts all JSON numbers to lossless numbers, then converts them back to the correct type based on the metadata * of a Dto class - fields decorated with `@Int64` are converted to a `string`, fields decorated with `@BigIntValue` are * converted to `bigint`. All other numbers are converted to `number`. Throws if a number cannot be safely converted. @@ -9,6 +12,9 @@ * * Update: added an optional `key` parameter to support the Camunda 8 REST API's use of an array under a key, e.g. { jobs : Job[] } * + * Note: the parser uses DTO classes that extend the LosslessDto class to perform mappings of numeric types. However, only the type of + * the annotated numerics is type-checked at runtime. Fields of other types are not checked. + * * More details on the design here: https://github.com/camunda/camunda-8-js-sdk/issues/81#issuecomment-2022213859 * * See this article to understand why this is necessary: https://jsoneditoronline.org/indepth/parse/why-does-json-parse-corrupt-large-numbers/ @@ -114,7 +120,7 @@ export function ChildDto(childClass: any) { * ``` */ export class LosslessDto { - constructor(obj: any) { + constructor(obj?: any) { if (obj) { for (const [key, value] of Object.entries(obj)) { this[key] = value @@ -123,13 +129,6 @@ export class LosslessDto { } } -export function losslessParseArray( - json: string, - dto?: { new (...args: any[]): T } -): T[] { - return losslessParse(json, dto) as T[] -} - /** * losslessParse uses lossless-json parse to deserialize JSON. * With no Dto, the parser will throw if it encounters an int64 number that cannot be safely represented as a JS number. @@ -142,11 +141,37 @@ export function losslessParse( dto?: { new (...args: any[]): T }, keyToParse?: string ): T { + /** + * lossless-json parse converts all numerics to LosslessNumber type instead of number type. + * Here we safely parse the string into an JSON object with all numerics as type LosslessNumber. + * This way we lose no fidelity at this stage, and can then use a supplied DTO to map large numbers + * or throw if we find an unsafe number. + */ const parsedLossless = parse(json) as any - // If keyToParse is provided, check for it in the parsed object - if (keyToParse && parsedLossless[keyToParse]) { - return losslessParse(stringify(parsedLossless[keyToParse]) as string, dto) + /** + * Specifying a keyToParse value applies all the mapping functionality to a key of the object in the JSON. + * gRPC API responses were naked objects or arrays of objects. REST response shapes typically have + * an array under an object key - eg: { jobs: [ ... ] } + * + * Since we now have a safely parsed object, we can recursively call losslessParse with the key, if it exists. + */ + if (keyToParse) { + if (parsedLossless[keyToParse]) { + return losslessParse(stringify(parsedLossless[keyToParse]) as string, dto) + } + /** + * A key was specified, but it was not found on the parsed object. + * At this point we should throw, because we cannot perform the operation requested. Something has gone wrong with + * the expected shape of the response. + * + * We throw an error with the actual shape of the object to help with debugging. + */ + throw new Error( + `Attempted to parse key ${keyToParse} on an object that does not have this key: ${stringify( + parsedLossless + )}` + ) } if (Array.isArray(parsedLossless)) { @@ -163,6 +188,10 @@ export function losslessParse( debug(`Got a Dto ${dto.name}. Parsing with annotations.`) const parsed = parseWithAnnotations(parsedLossless, dto) debug(`Converting remaining lossless numbers to numbers for ${dto.name}`) + /** All numbers are parsed to LosslessNumber by lossless-json. For any fields that should be numbers, we convert them + * now to number. Because we expose large values as string or BigInt, the only Lossless numbers left on the object + * are unmapped. So at this point we convert all remaining LosslessNumbers to number type if safe, and throw if not. + */ return convertLosslessNumbersToNumberOrThrow(parsed) } @@ -178,18 +207,18 @@ function parseWithAnnotations( if (Array.isArray(value)) { // If the value is an array, parse each element with the specified child class instance[key] = value.map((item) => - losslessParse(stringify(item) as string, childClass) + losslessParse(stringify(item)!, childClass) ) } else { // If the value is an object, parse it with the specified child class - instance[key] = losslessParse(stringify(value) as string, childClass) + instance[key] = losslessParse(stringify(value)!, childClass) } } else { if (Reflect.hasMetadata('type:int64', dto.prototype, key)) { debug(`Parsing int64 field "${key}" to string`) if (value) { if (isLosslessNumber(value)) { - instance[key] = (value as LosslessNumber).toString() + instance[key] = value.toString() } else { throw new Error( `Unexpected type: Received JSON ${typeof value} value for Int64String Dto field "${key}", expected number` @@ -200,7 +229,7 @@ function parseWithAnnotations( debug(`Parsing bigint field ${key}`) if (value) { if (isLosslessNumber(value)) { - instance[key] = BigInt((value as LosslessNumber).toString()) + instance[key] = BigInt(value.toString()) } else { throw new Error( `Unexpected type: Received JSON ${typeof value} value for BigIntValue Dto field "${key}", expected number` @@ -228,7 +257,13 @@ function parseArrayWithAnnotations( } /** - * Convert all `LosslessNumber` instances to a number or throw if any are unsafe + * Convert all `LosslessNumber` instances to a number or throw if any are unsafe. + * + * All numerics are converted to LosslessNumbers by lossless-json parse. Then, if a DTO was provided, + * all mappings have been done to either BigInt or string type. So all remaining LosslessNumbers in the object + * are either unmapped or mapped to number. + * + * Here we convert all remaining LosslessNumbers to a safe number value, or throw if an unsafe value is detected. */ function convertLosslessNumbersToNumberOrThrow(obj: any): T { debug(`Parsing LosslessNumbers to numbers for ${obj?.constructor?.name}`) diff --git a/src/zeebe/lib/GrpcClient.ts b/src/zeebe/lib/GrpcClient.ts index 069a16de..1c6c92e2 100644 --- a/src/zeebe/lib/GrpcClient.ts +++ b/src/zeebe/lib/GrpcClient.ts @@ -312,8 +312,6 @@ export class GrpcClient extends EventEmitter { this[`${methodName}Stream`] = async (data) => { debug(`Calling ${methodName}Stream...`, host) if (this.closing) { - // tslint:disable-next-line: no-console - console.log('Short-circuited on channel closed') // @DEBUG return } let stream: ClientReadableStream @@ -660,8 +658,7 @@ export class GrpcClient extends EventEmitter { if (isError) { if ( callStatus.code === 1 && - callStatus.details.includes('503') // || - // callStatus.code === 13 + callStatus.details.includes('503') // 'Service Unavailable' ) { return this.emit(MiddlewareSignals.Event.GrpcInterceptError, { callStatus, diff --git a/src/zeebe/lib/interfaces-grpc-1.0.ts b/src/zeebe/lib/interfaces-grpc-1.0.ts index 3b267eed..545c32ae 100644 --- a/src/zeebe/lib/interfaces-grpc-1.0.ts +++ b/src/zeebe/lib/interfaces-grpc-1.0.ts @@ -61,7 +61,7 @@ export interface ActivateJobsRequest { * To immediately complete the request when no job is activated set the requestTimeout to a negative value * */ - requestTimeout: MaybeTimeDuration + requestTimeout?: MaybeTimeDuration /** * a list of IDs of tenants for which to activate jobs */ From 8043ac9afe5f6f6ec6e8bdfa90dbf40def6a0510 Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Wed, 18 Sep 2024 14:05:11 +1200 Subject: [PATCH 09/34] feat(camunda8): complete deployResources feature --- src/__tests__/c8/rest/activateJobs.spec.ts | 28 ++-- .../oauth/OAuthProvider.unit.spec.ts | 6 +- src/c8/lib/C8Dto.ts | 80 +++++++++++ src/c8/lib/C8RestClient.ts | 133 +++++++++++++++--- 4 files changed, 209 insertions(+), 38 deletions(-) diff --git a/src/__tests__/c8/rest/activateJobs.spec.ts b/src/__tests__/c8/rest/activateJobs.spec.ts index 88e4bba4..7cf49f8b 100644 --- a/src/__tests__/c8/rest/activateJobs.spec.ts +++ b/src/__tests__/c8/rest/activateJobs.spec.ts @@ -1,36 +1,27 @@ +import path from 'node:path' + +import { DeployResourceResponse } from '../../../c8/lib/C8Dto' import { C8RestClient } from '../../../c8/lib/C8RestClient' import { restoreZeebeLogging, suppressZeebeLogging } from '../../../lib' -import { ZeebeGrpcClient } from '../../../zeebe' -import { DeployResourceResponse, ProcessDeployment } from '../../../zeebe/types' suppressZeebeLogging() -let res: DeployResourceResponse +let res: DeployResourceResponse let bpmnProcessId: string -const grpcClient = new ZeebeGrpcClient({ - config: { - CAMUNDA_TENANT_ID: '', - }, -}) const restClient = new C8RestClient() beforeAll(async () => { - res = (await restClient.deployResourcesFromFiles([ - './src/__tests__/testdata/hello-world-complete.bpmn', - ])) as unknown as DeployResourceResponse - // res = await grpcClient.deployResource({ - // processFilename: './src/__tests__/testdata/hello-world-complete.bpmn', - // }) - console.log(res) - bpmnProcessId = res.deployments[0].process.bpmnProcessId + res = await restClient.deployResourcesFromFiles([ + path.join('.', 'src', '__tests__', 'testdata', 'hello-world-complete.bpmn'), + ]) + bpmnProcessId = res.processes[0].bpmnProcessId }) afterAll(async () => { restoreZeebeLogging() - await grpcClient.close() }) test('Can service a task', (done) => { - grpcClient + restClient .createProcessInstance({ bpmnProcessId, variables: { @@ -48,7 +39,6 @@ test('Can service a task', (done) => { }) .then((jobs) => { expect(jobs.length).toBe(1) - jobs[0].complete().then(() => done()) }) }) diff --git a/src/__tests__/oauth/OAuthProvider.unit.spec.ts b/src/__tests__/oauth/OAuthProvider.unit.spec.ts index b2ee1f04..66331b94 100644 --- a/src/__tests__/oauth/OAuthProvider.unit.spec.ts +++ b/src/__tests__/oauth/OAuthProvider.unit.spec.ts @@ -249,13 +249,13 @@ describe('OAuthProvider', () => { }) it('Uses form encoding for request', (done) => { - const serverPort3001 = 3001 + const serverPort3010 = 3010 const o = new OAuthProvider({ config: { CAMUNDA_ZEEBE_OAUTH_AUDIENCE: 'token', ZEEBE_CLIENT_ID: 'clientId8', ZEEBE_CLIENT_SECRET: 'clientSecret', - CAMUNDA_OAUTH_URL: `http://127.0.0.1:${serverPort3001}`, + CAMUNDA_OAUTH_URL: `http://127.0.0.1:${serverPort3010}`, }, }) const secret = 'YOUR_SECRET' @@ -281,7 +281,7 @@ describe('OAuthProvider', () => { }) } }) - .listen(serverPort3001) + .listen(serverPort3010) o.getToken('OPERATE') }) diff --git a/src/c8/lib/C8Dto.ts b/src/c8/lib/C8Dto.ts index 218c823d..8d00dff2 100644 --- a/src/c8/lib/C8Dto.ts +++ b/src/c8/lib/C8Dto.ts @@ -58,3 +58,83 @@ export interface NewUserInfo { // eslint-disable-next-line @typescript-eslint/no-explicit-any export type Ctor = new (obj: any) => T + +export class ProcessDeployment extends LosslessDto { + bpmnProcessId!: string + version!: number + @Int64String + processDefinitionKey!: string + resourceName!: string + tenantId!: string +} + +export class DecisionDeployment extends LosslessDto { + dmnDecisionId!: string + version!: number + @Int64String + decisionKey!: string + dmnDecisionName!: string + tenantId!: string + dmnDecisionRequirementsId!: string + @Int64String + dmnDecisionRequirementsKey!: string +} + +export class DecisionRequirementsDeployment extends LosslessDto { + dmnDecisionRequirementsId!: string + version!: number + dmnDecisionRequirementsName!: string + tenantId!: string + @Int64String + dmnDecisionRequirementsKey!: string + resourceName!: string +} +export class FormDeployment { + formId!: string + version!: number + @Int64String + formKey!: string + resourceName!: string + tenantId!: string +} + +export class DeployResourceResponseDto extends LosslessDto { + @Int64String + key!: string + deployments!: ( + | { process: ProcessDeployment } + | { decision: DecisionDeployment } + | { decisionRequirements: DecisionRequirementsDeployment } + | { form: FormDeployment } + )[] + tenantId!: string +} + +export class DeployResourceResponse extends DeployResourceResponseDto { + processes!: ProcessDeployment[] + decisions!: DecisionDeployment[] + decisionRequirements!: DecisionRequirementsDeployment[] + forms!: FormDeployment[] +} + +export class CreateProcessInstanceResponse extends LosslessDto { + /** + * The unique key identifying the process definition (e.g. returned from a process + * in the DeployResourceResponse message) + */ + readonly processDefinitionKey!: string + /** + * The BPMN process ID of the process definition + */ + readonly bpmnProcessId!: string + /** + * The version of the process; set to -1 to use the latest version + */ + readonly version!: number + @Int64String + readonly processInstanceKey!: string + /** + * the tenant identifier of the created process instance + */ + readonly tenantId!: string +} diff --git a/src/c8/lib/C8RestClient.ts b/src/c8/lib/C8RestClient.ts index 968a2851..8f94bf55 100644 --- a/src/c8/lib/C8RestClient.ts +++ b/src/c8/lib/C8RestClient.ts @@ -3,6 +3,7 @@ import fs from 'node:fs' import { debug } from 'debug' import FormData from 'form-data' import got from 'got' +import { parse, stringify } from 'lossless-json' import { CamundaEnvironmentConfigurator, @@ -24,19 +25,32 @@ import { IOAuthProvider } from '../../oauth' import { ActivateJobsRequest, CompleteJobRequest, - CreateProcessInstanceRequest, + CreateProcessInstanceReq, ErrorJobWithVariables, FailJobRequest, IProcessVariables, Job, JOB_ACTION_ACKNOWLEDGEMENT, JobCompletionInterfaceRest, + JSONDoc, PublishMessageRequest, PublishMessageResponse, TopologyResponse, } from '../../zeebe/types' -import { Ctor, JobUpdateChangeset, NewUserInfo, TaskChangeSet } from './C8Dto' +import { + CreateProcessInstanceResponse, + Ctor, + DecisionDeployment, + DecisionRequirementsDeployment, + DeployResourceResponse, + DeployResourceResponseDto, + FormDeployment, + JobUpdateChangeset, + NewUserInfo, + ProcessDeployment, + TaskChangeSet, +} from './C8Dto' import { createSpecializedRestApiJobClass } from './RestApiJobClassFactory' const trace = debug('camunda:zeebe') @@ -384,14 +398,20 @@ export class C8RestClient { /** * Create and start a process instance */ - public async createProcessInstance(request: CreateProcessInstanceRequest) { + public async createProcessInstance( + request: CreateProcessInstanceReq + ) { const headers = await this.getHeaders() return this.rest.then((rest) => - rest.post(`process-instances`, { - body: losslessStringify(this.addDefaultTenantId(request)), - headers, - }) + rest + .post(`process-instances`, { + body: losslessStringify(this.addDefaultTenantId(request)), + headers, + parseJson: (text) => + losslessParse(text, CreateProcessInstanceResponse), + }) + .json() ) } @@ -420,7 +440,7 @@ export class C8RestClient { */ /** * Deploy resources to the broker. - * @param resources - An array of binary data buffers representing the resources to deploy. + * @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. */ public async deployResources( @@ -440,7 +460,7 @@ export class C8RestClient { formData.append('tenantId', tenantId ?? this.tenantId) } - return this.rest.then((rest) => + const res = await this.rest.then((rest) => rest .post('deployments', { body: formData, @@ -449,20 +469,101 @@ export class C8RestClient { ...formData.getHeaders(), Accept: 'application/json', }, - parseJson: (text) => losslessParse(text), + parseJson: (text) => parse(text), // we parse the response with LosslessNumbers, with no Dto }) - .json() + .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 DeployResourceResponse({ + key: res.key.toString(), + tenantId: res.tenantId, + deployments: [], + processes: [], + decisions: [], + decisionRequirements: [], + forms: [], + }) + + /** + * Type-guard assertions to correctly type the deployments. The API returns an array with mixed types. + */ + const isProcessDeployment = ( + deployment + ): deployment is { process: ProcessDeployment } => !!deployment.process + const isDecisionDeployment = ( + deployment + ): deployment is { decision: DecisionDeployment } => !!deployment.decision + const isDecisionRequirementsDeployment = ( + deployment + ): deployment is { decisionRequirements: DecisionRequirementsDeployment } => + !!deployment.decisionRequirements + const isFormDeployment = ( + deployment + ): deployment is { form: FormDeployment } => !!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 = losslessParse( + stringify(deployment.process)!, + ProcessDeployment + ) + deploymentResponse.deployments.push({ process: processDeployment }) + deploymentResponse.processes.push(processDeployment) + } + if (isDecisionDeployment(deployment)) { + const decisionDeployment = losslessParse( + stringify(deployment)!, + DecisionDeployment + ) + deploymentResponse.deployments.push({ decision: decisionDeployment }) + deploymentResponse.decisions.push(decisionDeployment) + } + if (isDecisionRequirementsDeployment(deployment)) { + const decisionRequirementsDeployment = losslessParse( + stringify(deployment)!, + DecisionRequirementsDeployment + ) + deploymentResponse.deployments.push({ + decisionRequirements: decisionRequirementsDeployment, + }) + deploymentResponse.decisionRequirements.push( + decisionRequirementsDeployment + ) + } + if (isFormDeployment(deployment)) { + const formDeployment = losslessParse( + stringify(deployment)!, + FormDeployment + ) + deploymentResponse.deployments.push({ form: formDeployment }) + deploymentResponse.forms.push(formDeployment) + } + }) + + return deploymentResponse } - public async deployResourcesFromFiles(filenames: string[]) { + /** + * Deploy resources to Camunda 8 from files + * @param files an array of file paths + */ + public async deployResourcesFromFiles(files: string[]) { const resources: { content: string; name: string }[] = [] - for (const filename of filenames) { - // const resource = await fs.promises.readFile(filename) + for (const file of files) { resources.push({ - content: fs.readFileSync(filename, { encoding: 'binary' }), - name: filename, + content: fs.readFileSync(file, { encoding: 'binary' }), + name: file, }) } From d41d3f8de6afe2d8d3daff207519a52119f26cc9 Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Wed, 18 Sep 2024 18:42:45 +1200 Subject: [PATCH 10/34] feat(camunda8): support pluggable winston logging for C8RestClient add additional REST methods and pluggable winston logging --- package-lock.json | 209 +++++++++++++++++- package.json | 3 +- src/__tests__/c8/rest/activateJobs.spec.ts | 10 +- ...rseJobs.spec.ts => parseJobs.unit.spec.ts} | 0 src/__tests__/c8/rest/pinClock.spec.ts | 10 + .../c8/rest/restMigrateProcess.spec.ts | 115 ++++++++++ src/c8/index.ts | 9 +- src/c8/lib/C8Dto.ts | 21 ++ src/c8/lib/C8JobWorker.ts | 155 +++++++++++++ src/c8/lib/C8Logger.ts | 40 ++++ src/c8/lib/C8RestClient.ts | 64 +++++- src/lib/Configuration.ts | 14 ++ src/zeebe/lib/interfaces-1.0.ts | 2 +- 13 files changed, 627 insertions(+), 25 deletions(-) rename src/__tests__/c8/rest/{parseJobs.spec.ts => parseJobs.unit.spec.ts} (100%) create mode 100644 src/__tests__/c8/rest/pinClock.spec.ts create mode 100644 src/__tests__/c8/rest/restMigrateProcess.spec.ts create mode 100644 src/c8/lib/C8JobWorker.ts create mode 100644 src/c8/lib/C8Logger.ts diff --git a/package-lock.json b/package-lock.json index 70651311..45b95862 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,8 @@ "reflect-metadata": "^0.2.1", "stack-trace": "0.0.10", "typed-duration": "^1.0.12", - "uuid": "^7.0.3" + "uuid": "^7.0.3", + "winston": "^3.14.2" }, "devDependencies": { "@commitlint/cli": "^18.4.3", @@ -1301,6 +1302,16 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "dev": true, @@ -3873,6 +3884,11 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" + }, "node_modules/@types/uuid": { "version": "9.0.8", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", @@ -4500,6 +4516,11 @@ "node": ">=0.10.0" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, "node_modules/asynckit": { "version": "0.4.0", "license": "MIT" @@ -5310,6 +5331,15 @@ "dev": true, "license": "MIT" }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, "node_modules/color-convert": { "version": "1.9.3", "license": "MIT", @@ -5321,6 +5351,15 @@ "version": "1.1.3", "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -5336,6 +5375,15 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "license": "MIT", @@ -6560,6 +6608,11 @@ "dev": true, "license": "MIT" }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -7712,6 +7765,11 @@ "bser": "2.1.1" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, "node_modules/figures": { "version": "3.2.0", "dev": true, @@ -7865,6 +7923,11 @@ "dev": true, "license": "ISC" }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, "node_modules/for-each": { "version": "0.3.3", "dev": true, @@ -8695,7 +8758,6 @@ }, "node_modules/inherits": { "version": "2.0.4", - "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -9017,7 +9079,6 @@ }, "node_modules/is-stream": { "version": "2.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10962,6 +11023,11 @@ "node": ">=6" } }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, "node_modules/leven": { "version": "3.1.0", "dev": true, @@ -11701,6 +11767,30 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/logform": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.1.tgz", + "integrity": "sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA==", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/long": { "version": "4.0.0", "license": "Apache-2.0" @@ -15057,6 +15147,14 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/onetime": { "version": "5.1.2", "dev": true, @@ -16292,7 +16390,6 @@ }, "node_modules/safe-buffer": { "version": "5.1.2", - "dev": true, "license": "MIT" }, "node_modules/safe-regex-test": { @@ -16308,6 +16405,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "dev": true, @@ -17009,6 +17114,19 @@ "node": ">=4" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, "node_modules/sisteransi": { "version": "1.0.5", "dev": true, @@ -17222,7 +17340,6 @@ }, "node_modules/string_decoder": { "version": "1.1.1", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -17553,6 +17670,11 @@ "node": ">=8" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, "node_modules/text-table": { "version": "0.2.0", "dev": true, @@ -17645,6 +17767,14 @@ "node": ">=8" } }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/ts-api-utils": { "version": "1.0.3", "dev": true, @@ -18344,7 +18474,6 @@ }, "node_modules/util-deprecate": { "version": "1.0.2", - "dev": true, "license": "MIT" }, "node_modules/utils-merge": { @@ -18527,6 +18656,74 @@ "node": ">=4" } }, + "node_modules/winston": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.14.2.tgz", + "integrity": "sha512-CO8cdpBB2yqzEf8v895L+GNKYJiEq8eKlHU38af3snQBQ+sdAIUepjMSguOIJC7ICbzm0ZI+Af2If4vIJrtmOg==", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.6.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.1.tgz", + "integrity": "sha512-wQCXXVgfv/wUPOfb2x0ruxzwkcZfxcktz6JIMUaPLmcNhO4bZTwA/WtDWK74xV3F2dKu8YadrFv0qhwYjVEwhA==", + "dependencies": { + "logform": "^2.6.1", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/winston/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/winston/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "dev": true, diff --git a/package.json b/package.json index 882853dd..dc1282ab 100644 --- a/package.json +++ b/package.json @@ -158,6 +158,7 @@ "reflect-metadata": "^0.2.1", "stack-trace": "0.0.10", "typed-duration": "^1.0.12", - "uuid": "^7.0.3" + "uuid": "^7.0.3", + "winston": "^3.14.2" } } diff --git a/src/__tests__/c8/rest/activateJobs.spec.ts b/src/__tests__/c8/rest/activateJobs.spec.ts index 7cf49f8b..8862d825 100644 --- a/src/__tests__/c8/rest/activateJobs.spec.ts +++ b/src/__tests__/c8/rest/activateJobs.spec.ts @@ -1,25 +1,17 @@ import path from 'node:path' -import { DeployResourceResponse } from '../../../c8/lib/C8Dto' import { C8RestClient } from '../../../c8/lib/C8RestClient' -import { restoreZeebeLogging, suppressZeebeLogging } from '../../../lib' -suppressZeebeLogging() -let res: DeployResourceResponse let bpmnProcessId: string const restClient = new C8RestClient() beforeAll(async () => { - res = await restClient.deployResourcesFromFiles([ + const res = await restClient.deployResourcesFromFiles([ path.join('.', 'src', '__tests__', 'testdata', 'hello-world-complete.bpmn'), ]) bpmnProcessId = res.processes[0].bpmnProcessId }) -afterAll(async () => { - restoreZeebeLogging() -}) - test('Can service a task', (done) => { restClient .createProcessInstance({ diff --git a/src/__tests__/c8/rest/parseJobs.spec.ts b/src/__tests__/c8/rest/parseJobs.unit.spec.ts similarity index 100% rename from src/__tests__/c8/rest/parseJobs.spec.ts rename to src/__tests__/c8/rest/parseJobs.unit.spec.ts diff --git a/src/__tests__/c8/rest/pinClock.spec.ts b/src/__tests__/c8/rest/pinClock.spec.ts new file mode 100644 index 00000000..e796b77e --- /dev/null +++ b/src/__tests__/c8/rest/pinClock.spec.ts @@ -0,0 +1,10 @@ +import { C8RestClient } from '../../../c8/lib/C8RestClient' + +test('We can pin the clock, and reset it', async () => { + const now = Date.now() + const c8 = new C8RestClient() + await c8.pinInternalClock(now) // Pin the clock to the present time + await c8.pinInternalClock(now + 1000) // Move the clock forward 1 second + await c8.resetClock() // Reset the clock + expect(now).toEqual(now) +}) diff --git a/src/__tests__/c8/rest/restMigrateProcess.spec.ts b/src/__tests__/c8/rest/restMigrateProcess.spec.ts new file mode 100644 index 00000000..9828dbbc --- /dev/null +++ b/src/__tests__/c8/rest/restMigrateProcess.spec.ts @@ -0,0 +1,115 @@ +import path from 'path' + +import { C8RestClient } from '../../../c8/lib/C8RestClient' + +import { LosslessDto } from './../../../lib' + +const c8 = new C8RestClient() + +class CustomHeaders extends LosslessDto { + ProcessVersion!: number +} + +test('RestClient can migrate a process instance', async () => { + // Deploy a process model + await c8.deployResourcesFromFiles([ + path.join( + '.', + 'src', + '__tests__', + 'testdata', + 'MigrateProcess-Version-1.bpmn' + ), + ]) + + // Create an instance of the process model + const processInstance = await c8.createProcessInstance({ + bpmnProcessId: 'migrant-work', + variables: {}, + }) + + let instanceKey = '' + let processVersion = 0 + + await new Promise((res) => { + const w = c8.createJobWorker({ + type: 'migrant-worker-task-1', + maxJobsToActivate: 10, + timeout: 30000, + pollIntervalMs: 1000, + worker: 'Migrant Worker 1', + customHeadersDto: CustomHeaders, + jobHandler: async (job) => { + instanceKey = job.processInstanceKey + processVersion = job.customHeaders.ProcessVersion as number + return job.complete().then((outcome) => { + w.stop() + res(null) + return outcome + }) + }, + }) + }) + + expect(instanceKey).toBe(processInstance.processInstanceKey) + expect(processVersion).toBe('1') + + // Deploy the updated process model + const res1 = await c8.deployResourcesFromFiles([ + './src/__tests__/testdata/MigrateProcess-Version-2.bpmn', + ]) + + // Migrate the process instance to the updated process model + + await c8.migrateProcessInstance({ + processInstanceKey: processInstance.processInstanceKey, + mappingInstructions: [ + { + sourceElementId: 'Activity_050vmrm', + targetElementId: 'Activity_050vmrm', + }, + ], + targetProcessDefinitionKey: res1.processes[0].processDefinitionKey, + }) + + // Complete the job in the process instance + + await new Promise((res) => { + const w = c8.createJobWorker({ + type: 'migration-checkpoint', + worker: 'Migrant Checkpoint worker', + maxJobsToActivate: 10, + timeout: 10000, + pollIntervalMs: 1000, + jobHandler: async (job) => { + return job.complete().then((outcome) => { + w.stop() + res(null) + return outcome + }) + }, + }) + }) + + await new Promise((res) => { + const w = c8.createJobWorker({ + type: 'migrant-worker-task-2', + worker: 'Migrant Worker 2', + maxJobsToActivate: 10, + timeout: 30000, + pollIntervalMs: 1000, + customHeadersDto: CustomHeaders, + jobHandler: async (job) => { + instanceKey = job.processInstanceKey + processVersion = job.customHeaders.ProcessVersion as number + return job.complete().then((outcome) => { + w.stop() + res(null) + return outcome + }) + }, + }) + }) + expect(instanceKey).toBe(processInstance.processInstanceKey) + expect(processVersion).toBe('2') +}) diff --git a/src/c8/index.ts b/src/c8/index.ts index a0cceaeb..44517ebc 100644 --- a/src/c8/index.ts +++ b/src/c8/index.ts @@ -1,9 +1,11 @@ +import winston from 'winston' + import { AdminApiClient } from '../admin' import { + Camunda8ClientConfiguration, CamundaEnvironmentConfigurator, CamundaPlatform8Configuration, constructOAuthProvider, - DeepPartial, } from '../lib' import { ModelerApiClient } from '../modeler' import { IOAuthProvider } from '../oauth' @@ -12,6 +14,7 @@ import { OptimizeApiClient } from '../optimize' import { TasklistApiClient } from '../tasklist' import { ZeebeGrpcClient, ZeebeRestClient } from '../zeebe' +import { getLogger } from './lib/C8Logger' import { C8RestClient } from './lib/C8RestClient' /** @@ -44,11 +47,13 @@ export class Camunda8 { private configuration: CamundaPlatform8Configuration private oAuthProvider: IOAuthProvider private c8RestClient?: C8RestClient + public log: winston.Logger - constructor(config: DeepPartial = {}) { + constructor(config: Camunda8ClientConfiguration = {}) { this.configuration = CamundaEnvironmentConfigurator.mergeConfigWithEnvironment(config) this.oAuthProvider = constructOAuthProvider(this.configuration) + this.log = getLogger(config) } public getOperateApiClient(): OperateApiClient { diff --git a/src/c8/lib/C8Dto.ts b/src/c8/lib/C8Dto.ts index 8d00dff2..66945541 100644 --- a/src/c8/lib/C8Dto.ts +++ b/src/c8/lib/C8Dto.ts @@ -1,3 +1,5 @@ +import { LosslessNumber } from 'lossless-json' + import { Int64String, LosslessDto } from '../../lib' export class RestApiJob< @@ -138,3 +140,22 @@ export class CreateProcessInstanceResponse extends LosslessDto { */ readonly tenantId!: string } + +export interface MigrationMappingInstruction { + /** The element ID to migrate from. */ + sourceElementId: string + /** The element ID to migrate into. */ + targetElementId: string +} + +/** 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. + */ +export interface MigrationRequest { + processInstanceKey: string + /** The key of process definition to migrate the process instance to. */ + targetProcessDefinitionKey: string + mappingInstructions: MigrationMappingInstruction[] + /** A reference key chosen by the user that will be part of all records resulting from this operation. Must be > 0 if provided. */ + operationReference?: number | LosslessNumber +} diff --git a/src/c8/lib/C8JobWorker.ts b/src/c8/lib/C8JobWorker.ts new file mode 100644 index 00000000..01d4f6e2 --- /dev/null +++ b/src/c8/lib/C8JobWorker.ts @@ -0,0 +1,155 @@ +import winston from 'winston' + +import { LosslessDto } from '../../lib' +import { + ActivateJobsRequest, + IProcessVariables, + Job, + JobCompletionInterfaceRest, + MustReturnJobActionAcknowledgement, +} from '../../zeebe/types' + +import { Ctor } from './C8Dto' +import { getLogger } from './C8Logger' +import { C8RestClient } from './C8RestClient' + +export interface C8JobWorkerConfig< + VariablesDto extends LosslessDto, + CustomHeadersDto extends LosslessDto, +> extends ActivateJobsRequest { + inputVariableDto?: Ctor + customHeadersDto?: Ctor + /** How often the worker will poll for new jobs. Defaults to 30s */ + pollIntervalMs?: number + jobHandler: ( + job: Job & + JobCompletionInterfaceRest, + log: winston.Logger + ) => MustReturnJobActionAcknowledgement + logger?: winston.Logger +} + +export class C8JobWorker< + VariablesDto extends LosslessDto, + CustomHeadersDto extends LosslessDto, +> { + public currentlyActiveJobCount = 0 + public capacity: number + private loopHandle?: NodeJS.Timeout + private pollInterval: number + private log: winston.Logger + + constructor( + private readonly config: C8JobWorkerConfig, + private readonly restClient: C8RestClient + ) { + this.pollInterval = config.pollIntervalMs ?? 30000 + this.capacity = this.config.maxJobsToActivate + this.log = getLogger({ logger: config.logger }) + this.log.debug( + `Created REST Job Worker '${this.config.worker}' for job type '${this.config.type}', polling every ${this.pollInterval}ms, capacity ${this.config.maxJobsToActivate}` + ) + } + + start() { + this.log.debug( + `Starting poll loop for worker '${this.config.worker}', polling for job type '${this.config.type}' every ${this.pollInterval}ms` + ) + this.poll() + this.loopHandle = setInterval(() => this.poll, this.pollInterval) + } + + async stop(timeoutMs = 30000) { + const worker = this.config.worker + this.log.debug( + `Stop requested for worker '${worker}', working on job type '${this.config.type}'` + ) + clearInterval(this.loopHandle) + return new Promise((resolve, reject) => { + if (this.currentlyActiveJobCount === 0) { + this.log.debug(`Worker '${worker}' - all jobs drained. Worker stopped.`) + return resolve(null) + } + this.log.debug( + `Worker '${worker}' - ${this.currentlyActiveJobCount} jobs currently active.` + ) + const timeout = setTimeout(() => { + clearInterval(wait) + return reject(`Failed to drain all jobs in ${timeoutMs}ms`) + }, timeoutMs) + const wait = setInterval(() => { + if (this.currentlyActiveJobCount === 0) { + this.log.debug( + `Worker '${worker}' - all jobs drained. Worker stopped.` + ) + clearInterval(wait) + clearTimeout(timeout) + return resolve(null) + } + this.log.debug( + `Worker '${worker}' - ${this.currentlyActiveJobCount} jobs currently active.` + ) + }, 1000) + }) + } + + private poll() { + if (this.currentlyActiveJobCount >= this.config.maxJobsToActivate) { + this.log.debug( + `Worker '${this.config.worker}' at capacity ${this.currentlyActiveJobCount} for job type '${this.config.type}'` + ) + return + } + + this.log.silly( + `Worker: '${this.config.worker}' for job type '${this.config.type}' polling for jobs` + ) + + const remainingJobCapacity = + this.config.maxJobsToActivate - this.currentlyActiveJobCount + this.restClient + .activateJobs({ + ...this.config, + maxJobsToActivate: remainingJobCapacity, + }) + .then((jobs) => { + const count = jobs.length + this.log.debug( + `Worker '${this.config.worker}' for job type '${this.config.type}' activated ${count} jobs` + ) + this.currentlyActiveJobCount += count + // The job handlers for the activated jobs will run in parallel + jobs.forEach((job) => this.handleJob(job)) + }) + } + + private async handleJob( + job: Job & + JobCompletionInterfaceRest + ) { + try { + this.log.debug( + `Invoking job handler for job ${job.key} of type '${job.type}'` + ) + await this.config.jobHandler(job, this.log) + this.log.debug( + `Completed job handler for job ${job.key} of type '${job.type}'.` + ) + } catch (e) { + /** Unhandled exception in the job handler */ + this.log.error( + `Unhandled exception in job handler for job ${job.key} - Worker: ${this.config.worker} for job type: '${this.config.type}'` + ) + this.log.error(e) + this.log.error((e as Error).stack) + this.log.error(`Failing the job`) + await job.fail({ + errorMessage: (e as Error).toString(), + retries: job.retries - 1, + }) + } finally { + /** Decrement the active job count in all cases */ + this.currentlyActiveJobCount-- + } + } +} diff --git a/src/c8/lib/C8Logger.ts b/src/c8/lib/C8Logger.ts new file mode 100644 index 00000000..fe3111fe --- /dev/null +++ b/src/c8/lib/C8Logger.ts @@ -0,0 +1,40 @@ +import winston from 'winston' // Import Winston + +import { + Camunda8ClientConfiguration, + CamundaEnvironmentConfigurator, +} from '../../lib' + +let defaultLogger: winston.Logger +let cachedLogger: winston.Logger | undefined + +export function getLogger(config?: Camunda8ClientConfiguration) { + const configuration = + CamundaEnvironmentConfigurator.mergeConfigWithEnvironment(config ?? {}) + // We assume that the SDK user uses a single winston instance for 100% of logging, or no logger at all (in which case we create our own) + if (config?.logger && cachedLogger !== config.logger) { + cachedLogger = config.logger + config.logger.info( + `Using supplied winston logger at level '${config.logger.level}'` + ) + } + if (!defaultLogger) { + // Define the default logger + defaultLogger = winston.createLogger({ + level: configuration.CAMUNDA_LOG_LEVEL, + format: winston.format.combine( + winston.format.timestamp(), + winston.format.colorize(), + winston.format.simple() + ), + transports: [new winston.transports.Console()], + }) + } + if (!cachedLogger) { + defaultLogger.info( + `Using default winston logger at level '${defaultLogger.level}'` + ) + cachedLogger = defaultLogger + } + return config?.logger ?? defaultLogger +} diff --git a/src/c8/lib/C8RestClient.ts b/src/c8/lib/C8RestClient.ts index 8f94bf55..c9997d7a 100644 --- a/src/c8/lib/C8RestClient.ts +++ b/src/c8/lib/C8RestClient.ts @@ -4,13 +4,13 @@ import { debug } from 'debug' import FormData from 'form-data' import got from 'got' import { parse, stringify } from 'lossless-json' +import winston from 'winston' import { + Camunda8ClientConfiguration, CamundaEnvironmentConfigurator, - CamundaPlatform8Configuration, constructOAuthProvider, createUserAgentString, - DeepPartial, GetCustomCertificateBuffer, gotBeforeErrorHook, gotErrorHandler, @@ -47,10 +47,13 @@ import { DeployResourceResponseDto, FormDeployment, JobUpdateChangeset, + MigrationRequest, NewUserInfo, ProcessDeployment, TaskChangeSet, } from './C8Dto' +import { C8JobWorker, C8JobWorkerConfig } from './C8JobWorker' +import { getLogger } from './C8Logger' import { createSpecializedRestApiJobClass } from './RestApiJobClassFactory' const trace = debug('camunda:zeebe') @@ -62,14 +65,17 @@ export class C8RestClient { private oAuthProvider: IOAuthProvider private rest: Promise private tenantId?: string + private log: winston.Logger constructor(options?: { - config?: DeepPartial + config?: Camunda8ClientConfiguration oAuthProvider?: IOAuthProvider }) { const config = CamundaEnvironmentConfigurator.mergeConfigWithEnvironment( options?.config ?? {} ) + this.log = getLogger(config) + this.log.info(`Using REST API version ${CAMUNDA_REST_API_VERSION}`) trace('options.config', options?.config) trace('config', config) this.oAuthProvider = @@ -259,6 +265,14 @@ export class C8RestClient { return this.rest.then((rest) => rest.get(`license`).json()) } + public createJobWorker< + Variables extends LosslessDto, + CustomHeaders extends LosslessDto, + >(config: C8JobWorkerConfig) { + const worker = new C8JobWorker(config, this) + worker.start() + return worker + } /** * Iterate through all known partitions and activate jobs up to the requested maximum. * @@ -423,7 +437,7 @@ export class C8RestClient { operationReference, }: { processInstanceKey: string - operationReference?: string + operationReference?: number }) { const headers = await this.getHeaders() @@ -436,8 +450,21 @@ export class C8RestClient { } /** - * Deploy resources to the broker + * 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. */ + public async migrateProcessInstance(req: MigrationRequest) { + const headers = await this.getHeaders() + const { processInstanceKey, ...request } = req + return this.rest.then((rest) => + rest.post(`process-instances/${processInstanceKey}/migration`, { + headers, + body: losslessStringify(request), + }) + ) + } + /** * Deploy resources to the broker. * @param resources - An array of binary data strings representing the resources to deploy. @@ -570,6 +597,31 @@ export class C8RestClient { return this.deployResources(resources) } + /** + * 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. + */ + public async pinInternalClock(epochMs: number) { + const headers = await this.getHeaders() + + return this.rest.then((rest) => + rest.put(`clock`, { + headers, + body: JSON.stringify({ 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. + */ + public async resetClock() { + const headers = await this.getHeaders() + return this.rest.then((rest) => rest.post(`clock/reset`, { headers })) + } + private addJobMethods = ( job: Job ): Job & @@ -590,7 +642,7 @@ export class C8RestClient { jobKey: job.key, }), fail: (failJobRequest) => this.failJob(failJobRequest), - /* At this point, no capacity handling in the SDK is implemented, so this has no effect */ + /* This has an effect in a Job Worker, decrementing the currently active job count */ forward: () => JOB_ACTION_ACKNOWLEDGEMENT, modifyJobTimeout: ({ newTimeoutMs }: { newTimeoutMs: number }) => this.updateJob({ jobKey: job.key, timeout: newTimeoutMs }), diff --git a/src/lib/Configuration.ts b/src/lib/Configuration.ts index 9d56b592..6bcf7292 100644 --- a/src/lib/Configuration.ts +++ b/src/lib/Configuration.ts @@ -1,5 +1,6 @@ import mergeWith from 'lodash.mergewith' import { createEnv } from 'neon-env' +import winston from 'winston' const getMainEnv = () => createEnv({ @@ -20,6 +21,13 @@ const getMainEnv = () => optional: true, default: 1000, }, + /** The log level for logging. Defaults to 'info'. Values (in order of priority): 'error', 'warn', 'info', 'http', 'verbose', 'debug', 'silly' */ + CAMUNDA_LOG_LEVEL: { + type: 'string', + optional: true, + choices: ['error', 'warn', 'info', 'http', 'verbose', 'debug', 'silly'], + default: 'info', + }, /** The address for the Zeebe GRPC. */ ZEEBE_GRPC_ADDRESS: { type: 'string', @@ -378,6 +386,7 @@ export const CamundaEnvironmentVariableDictionary = 'CAMUNDA_CONSOLE_CLIENT_ID', 'CAMUNDA_CONSOLE_CLIENT_SECRET', 'CAMUNDA_CONSOLE_OAUTH_AUDIENCE', + 'CAMUNDA_LOG_LEVEL', 'CAMUNDA_MODELER_BASE_URL', 'CAMUNDA_MODELER_OAUTH_AUDIENCE', 'CAMUNDA_OPERATE_BASE_URL', @@ -429,3 +438,8 @@ export type CamundaPlatform8Configuration = ReturnType< export type DeepPartial = { [K in keyof T]?: T[K] extends object ? DeepPartial : T[K] } + +export type Camunda8ClientConfiguration = + DeepPartial & { + logger?: winston.Logger + } diff --git a/src/zeebe/lib/interfaces-1.0.ts b/src/zeebe/lib/interfaces-1.0.ts index 80079ff2..e921c615 100644 --- a/src/zeebe/lib/interfaces-1.0.ts +++ b/src/zeebe/lib/interfaces-1.0.ts @@ -494,7 +494,7 @@ export interface MigrateProcessInstanceReq { // the migration plan that defines target process and element mappings migrationPlan: MigrationPlan /** a reference key chosen by the user and will be part of all records resulted from this operation */ - operationReference?: string + operationReference?: number | LosslessNumber } export interface ZBGrpc extends GrpcClient { From 43f82a44f69f0217003775d422c741555f1fe6b1 Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Wed, 18 Sep 2024 19:26:46 +1200 Subject: [PATCH 11/34] feat(camunda8): support broadcastSignal over REST fixes #248 --- .../c8/rest/restBroadcastSignal.spec.ts | 52 +++++++++++++++++++ src/c8/lib/C8Dto.ts | 9 ++++ src/c8/lib/C8RestClient.ts | 29 ++++++++--- 3 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 src/__tests__/c8/rest/restBroadcastSignal.spec.ts diff --git a/src/__tests__/c8/rest/restBroadcastSignal.spec.ts b/src/__tests__/c8/rest/restBroadcastSignal.spec.ts new file mode 100644 index 00000000..0039f68f --- /dev/null +++ b/src/__tests__/c8/rest/restBroadcastSignal.spec.ts @@ -0,0 +1,52 @@ +import { C8RestClient } from '../../../c8/lib/C8RestClient' +import { LosslessDto } from '../../../lib' +import { cancelProcesses } from '../../../zeebe/lib/cancelProcesses' + +jest.setTimeout(60000) + +const c8 = new C8RestClient() +let pid: string + +beforeAll(async () => { + const res = await c8.deployResourcesFromFiles([ + './src/__tests__/testdata/Signal.bpmn', + ]) + pid = res.processes[0].processDefinitionKey + await cancelProcesses(pid) +}) + +afterAll(async () => { + await cancelProcesses(pid) +}) + +test('Can start a process with a signal', async () => { + await c8.deployResourcesFromFiles(['./src/__tests__/testdata/Signal.bpmn']) + + const res = await c8.broadcastSignal({ + signalName: 'test-signal', + variables: { + success: true, + }, + }) + + expect(res.key).toBeTruthy() + + await new Promise((resolve) => + c8.createJobWorker({ + type: 'signal-service-task', + worker: 'signal-worker', + timeout: 10000, + pollIntervalMs: 1000, + maxJobsToActivate: 10, + inputVariableDto: class extends LosslessDto { + success!: boolean + }, + jobHandler: (job) => { + const ack = job.complete() + expect(job.variables.success).toBe(true) + resolve(null) + return ack + }, + }) + ) +}) diff --git a/src/c8/lib/C8Dto.ts b/src/c8/lib/C8Dto.ts index 66945541..22fe6df3 100644 --- a/src/c8/lib/C8Dto.ts +++ b/src/c8/lib/C8Dto.ts @@ -159,3 +159,12 @@ export interface MigrationRequest { /** A reference key chosen by the user that will be part of all records resulting from this operation. Must be > 0 if provided. */ operationReference?: number | LosslessNumber } + +/** The signal was broadcast. */ +export class BroadcastSignalResponse extends LosslessDto { + @Int64String + /** The unique ID of the signal that was broadcast. */ + key!: string + /** The tenant ID of the signal that was broadcast. */ + tenantId!: string +} diff --git a/src/c8/lib/C8RestClient.ts b/src/c8/lib/C8RestClient.ts index c9997d7a..7e82849f 100644 --- a/src/c8/lib/C8RestClient.ts +++ b/src/c8/lib/C8RestClient.ts @@ -24,6 +24,7 @@ import { import { IOAuthProvider } from '../../oauth' import { ActivateJobsRequest, + BroadcastSignalReq, CompleteJobRequest, CreateProcessInstanceReq, ErrorJobWithVariables, @@ -39,6 +40,7 @@ import { } from '../../zeebe/types' import { + BroadcastSignalResponse, CreateProcessInstanceResponse, Ctor, DecisionDeployment, @@ -124,18 +126,29 @@ export class C8RestClient { return headers } - /* Get the topology of the Zeebe cluster. */ - public async getTopology(): Promise { + /** + * Broadcasts a signal. + */ + public async broadcastSignal(req: BroadcastSignalReq) { const headers = await this.getHeaders() + const request = this.addDefaultTenantId(req) return this.rest.then((rest) => rest - .get('topology', { headers }) - .json() - .catch((error) => { - trace('error', error) - throw error + .post(`signals/broadcast`, { + headers, + body: stringify(request), + parseJson: (text) => losslessParse(text, BroadcastSignalResponse), }) - ) as Promise + .json() + ) + } + + /* Get the topology of the Zeebe cluster. */ + public async getTopology() { + const headers = await this.getHeaders() + return this.rest.then((rest) => + rest.get('topology', { headers }).json() + ) } /* Completes a user task with the given key. The method either completes the task or throws 400, 404, or 409. From fead376a7f42619cec9b956834f90d5fd2cd6a66 Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Wed, 18 Sep 2024 19:31:52 +1200 Subject: [PATCH 12/34] test(camunda8): stop worker in broadcastSignal test --- src/__tests__/c8/rest/restBroadcastSignal.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/__tests__/c8/rest/restBroadcastSignal.spec.ts b/src/__tests__/c8/rest/restBroadcastSignal.spec.ts index 0039f68f..353448e2 100644 --- a/src/__tests__/c8/rest/restBroadcastSignal.spec.ts +++ b/src/__tests__/c8/rest/restBroadcastSignal.spec.ts @@ -31,8 +31,8 @@ test('Can start a process with a signal', async () => { expect(res.key).toBeTruthy() - await new Promise((resolve) => - c8.createJobWorker({ + await new Promise((resolve) => { + const w = c8.createJobWorker({ type: 'signal-service-task', worker: 'signal-worker', timeout: 10000, @@ -44,9 +44,9 @@ test('Can start a process with a signal', async () => { jobHandler: (job) => { const ack = job.complete() expect(job.variables.success).toBe(true) - resolve(null) + w.stop().then(() => resolve(null)) return ack }, }) - ) + }) }) From 7de82b75c9a08fe34444d549e3c9c2077a768b3e Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Wed, 18 Sep 2024 21:26:51 +1200 Subject: [PATCH 13/34] feat(camunda8): support updateElementInstanceVariables fixes #249 --- src/c8/lib/C8RestClient.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/c8/lib/C8RestClient.ts b/src/c8/lib/C8RestClient.ts index 7e82849f..49c03baf 100644 --- a/src/c8/lib/C8RestClient.ts +++ b/src/c8/lib/C8RestClient.ts @@ -53,6 +53,7 @@ import { NewUserInfo, ProcessDeployment, TaskChangeSet, + UpdateElementVariableRequest, } from './C8Dto' import { C8JobWorker, C8JobWorkerConfig } from './C8JobWorker' import { getLogger } from './C8Logger' @@ -278,6 +279,10 @@ export class C8RestClient { 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. + */ public createJobWorker< Variables extends LosslessDto, CustomHeaders extends LosslessDto, @@ -470,6 +475,9 @@ export class C8RestClient { public async migrateProcessInstance(req: MigrationRequest) { const headers = await this.getHeaders() const { processInstanceKey, ...request } = req + this.log.debug(`Migrating process instance ${processInstanceKey}`, { + component: 'C8RestClient', + }) return this.rest.then((rest) => rest.post(`process-instances/${processInstanceKey}/migration`, { headers, @@ -500,6 +508,7 @@ export class C8RestClient { formData.append('tenantId', tenantId ?? this.tenantId) } + this.log.debug(`Deploying ${resources.length} resources`) const res = await this.rest.then((rest) => rest .post('deployments', { @@ -662,6 +671,23 @@ export class C8RestClient { } } + /** + * 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. + */ + public async updateElementInstanceVariables( + req: UpdateElementVariableRequest + ) { + const headers = await this.getHeaders() + const { elementInstanceKey, ...request } = req + return this.rest.then((rest) => + rest.post(`element-instances/${elementInstanceKey}/variables`, { + headers, + body: stringify(request), + }) + ) + } + /** * Helper method to add the default tenantIds if we are not passed explicit tenantIds */ From d77a0c2e2f4b2698dbdeae02ece1176da853dc10 Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Wed, 18 Sep 2024 21:28:19 +1200 Subject: [PATCH 14/34] test(camunda8): fix tests for migrateProcess over Rest --- .../c8/rest/restMigrateProcess.spec.ts | 42 ++++---- .../MigrateProcess-Rest-Version-1.bpmn | 91 ++++++++++++++++ .../MigrateProcess-Rest-Version-2.bpmn | 91 ++++++++++++++++ .../integration/Client-DeployResource.spec.ts | 2 +- src/c8/lib/C8Dto.ts | 24 +++++ src/c8/lib/C8JobWorker.ts | 100 +++++++++++------- 6 files changed, 287 insertions(+), 63 deletions(-) create mode 100644 src/__tests__/testdata/MigrateProcess-Rest-Version-1.bpmn create mode 100644 src/__tests__/testdata/MigrateProcess-Rest-Version-2.bpmn diff --git a/src/__tests__/c8/rest/restMigrateProcess.spec.ts b/src/__tests__/c8/rest/restMigrateProcess.spec.ts index 9828dbbc..fdfff7c6 100644 --- a/src/__tests__/c8/rest/restMigrateProcess.spec.ts +++ b/src/__tests__/c8/rest/restMigrateProcess.spec.ts @@ -1,5 +1,7 @@ import path from 'path' +import { C8JobWorker } from 'c8/lib/C8JobWorker' + import { C8RestClient } from '../../../c8/lib/C8RestClient' import { LosslessDto } from './../../../lib' @@ -18,22 +20,22 @@ test('RestClient can migrate a process instance', async () => { 'src', '__tests__', 'testdata', - 'MigrateProcess-Version-1.bpmn' + 'MigrateProcess-Rest-Version-1.bpmn' ), ]) // Create an instance of the process model const processInstance = await c8.createProcessInstance({ - bpmnProcessId: 'migrant-work', + bpmnProcessId: 'migrant-work-rest', variables: {}, }) let instanceKey = '' let processVersion = 0 - await new Promise((res) => { + await new Promise>((res) => { const w = c8.createJobWorker({ - type: 'migrant-worker-task-1', + type: 'migrant-rest-worker-task-1', maxJobsToActivate: 10, timeout: 30000, pollIntervalMs: 1000, @@ -42,21 +44,20 @@ test('RestClient can migrate a process instance', async () => { jobHandler: async (job) => { instanceKey = job.processInstanceKey processVersion = job.customHeaders.ProcessVersion as number - return job.complete().then((outcome) => { - w.stop() - res(null) + return job.complete().then(async (outcome) => { + res(w) return outcome }) }, }) - }) + }).then((w) => w.stop()) expect(instanceKey).toBe(processInstance.processInstanceKey) expect(processVersion).toBe('1') // Deploy the updated process model const res1 = await c8.deployResourcesFromFiles([ - './src/__tests__/testdata/MigrateProcess-Version-2.bpmn', + './src/__tests__/testdata/MigrateProcess-Rest-Version-2.bpmn', ]) // Migrate the process instance to the updated process model @@ -74,26 +75,25 @@ test('RestClient can migrate a process instance', async () => { // Complete the job in the process instance - await new Promise((res) => { + await new Promise>((res) => { const w = c8.createJobWorker({ - type: 'migration-checkpoint', + type: 'migration-rest-checkpoint', worker: 'Migrant Checkpoint worker', maxJobsToActivate: 10, timeout: 10000, pollIntervalMs: 1000, jobHandler: async (job) => { - return job.complete().then((outcome) => { - w.stop() - res(null) + return job.complete().then(async (outcome) => { + res(w) return outcome }) }, }) - }) + }).then((w) => w.stop()) - await new Promise((res) => { + await new Promise>((res) => { const w = c8.createJobWorker({ - type: 'migrant-worker-task-2', + type: 'migrant-rest-worker-task-2', worker: 'Migrant Worker 2', maxJobsToActivate: 10, timeout: 30000, @@ -102,14 +102,14 @@ test('RestClient can migrate a process instance', async () => { jobHandler: async (job) => { instanceKey = job.processInstanceKey processVersion = job.customHeaders.ProcessVersion as number - return job.complete().then((outcome) => { - w.stop() - res(null) + return job.complete().then(async (outcome) => { + res(w) return outcome }) }, }) - }) + }).then((w) => w.stop()) + expect(instanceKey).toBe(processInstance.processInstanceKey) expect(processVersion).toBe('2') }) diff --git a/src/__tests__/testdata/MigrateProcess-Rest-Version-1.bpmn b/src/__tests__/testdata/MigrateProcess-Rest-Version-1.bpmn new file mode 100644 index 00000000..8ff09430 --- /dev/null +++ b/src/__tests__/testdata/MigrateProcess-Rest-Version-1.bpmn @@ -0,0 +1,91 @@ + + + + + Flow_167nn02 + + + + Flow_1r250pk + + + + + + + + + Flow_167nn02 + Flow_04fsyv6 + + + + + + + + + + + Flow_1igeic8 + Flow_1r250pk + + + + + + + Flow_04fsyv6 + Flow_1igeic8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/__tests__/testdata/MigrateProcess-Rest-Version-2.bpmn b/src/__tests__/testdata/MigrateProcess-Rest-Version-2.bpmn new file mode 100644 index 00000000..c26943fb --- /dev/null +++ b/src/__tests__/testdata/MigrateProcess-Rest-Version-2.bpmn @@ -0,0 +1,91 @@ + + + + + Flow_167nn02 + + + + Flow_1r250pk + + + + + + + + + Flow_167nn02 + Flow_04fsyv6 + + + + + + + + + + + Flow_1igeic8 + Flow_1r250pk + + + + + + + Flow_04fsyv6 + Flow_1igeic8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/__tests__/zeebe/integration/Client-DeployResource.spec.ts b/src/__tests__/zeebe/integration/Client-DeployResource.spec.ts index 3f6419a9..a46a74c5 100644 --- a/src/__tests__/zeebe/integration/Client-DeployResource.spec.ts +++ b/src/__tests__/zeebe/integration/Client-DeployResource.spec.ts @@ -61,7 +61,7 @@ test('deploys a Form', async () => { }) expect(result.deployments[0].form).not.toBeNull() }) -test.only('deploys multiple resources', async () => { +test('deploys multiple resources', async () => { const result = await zbc.deployResources([ { processFilename: './src/__tests__/testdata/Client-DeployWorkflow.bpmn', diff --git a/src/c8/lib/C8Dto.ts b/src/c8/lib/C8Dto.ts index 22fe6df3..072128b8 100644 --- a/src/c8/lib/C8Dto.ts +++ b/src/c8/lib/C8Dto.ts @@ -1,6 +1,7 @@ import { LosslessNumber } from 'lossless-json' import { Int64String, LosslessDto } from '../../lib' +import { JSONDoc } from '../../zeebe/types' export class RestApiJob< Variables = LosslessDto, @@ -168,3 +169,26 @@ export class BroadcastSignalResponse extends LosslessDto { /** The tenant ID of the signal that was broadcast. */ tenantId!: string } + +export interface UpdateElementVariableRequest { + /** + * The key of the element instance to update the variables for. + * This can be the process instance key (as obtained during instance creation), or a given element, + * such as a service task (see the elementInstanceKey on the job message). */ + elementInstanceKey: string + variables: JSONDoc | LosslessDto + /** + * Defaults to false. + * If set to true, the variables are merged strictly into the local scope (as specified by the elementInstanceKey). Otherwise, the variables are propagated to upper scopes and set at the outermost one. + * Let’s consider the following example: + * There are two scopes '1' and '2'. Scope '1' is the parent scope of '2'. The effective variables of the scopes are: 1 => { "foo" : 2 } 2 => { "bar" : 1 } + * An update request with elementInstanceKey as '2', variables { "foo" : 5 }, and local set to true leaves scope '1' unchanged and adjusts scope '2' to { "bar" : 1, "foo" 5 }. + * By default, with local set to false, scope '1' will be { "foo": 5 } and scope '2' will be { "bar" : 1 }. + */ + local?: boolean + /** + * A reference key chosen by the user that will be part of all records resulting from this operation. + * Must be > 0 if provided. + */ + operationReference?: number +} diff --git a/src/c8/lib/C8JobWorker.ts b/src/c8/lib/C8JobWorker.ts index 01d4f6e2..813646f3 100644 --- a/src/c8/lib/C8JobWorker.ts +++ b/src/c8/lib/C8JobWorker.ts @@ -38,6 +38,13 @@ export class C8JobWorker< private loopHandle?: NodeJS.Timeout private pollInterval: number private log: winston.Logger + logMeta: () => { + worker: string + type: string + pollInterval: number + capacity: number + currentload: number + } constructor( private readonly config: C8JobWorkerConfig, @@ -46,64 +53,66 @@ export class C8JobWorker< this.pollInterval = config.pollIntervalMs ?? 30000 this.capacity = this.config.maxJobsToActivate this.log = getLogger({ logger: config.logger }) - this.log.debug( - `Created REST Job Worker '${this.config.worker}' for job type '${this.config.type}', polling every ${this.pollInterval}ms, capacity ${this.config.maxJobsToActivate}` - ) + this.logMeta = () => ({ + worker: this.config.worker, + type: this.config.type, + pollInterval: this.pollInterval, + capacity: this.config.maxJobsToActivate, + currentload: this.currentlyActiveJobCount, + }) + this.log.debug(`Created REST Job Worker`, this.logMeta()) } start() { - this.log.debug( - `Starting poll loop for worker '${this.config.worker}', polling for job type '${this.config.type}' every ${this.pollInterval}ms` - ) + this.log.debug(`Starting poll loop`, this.logMeta()) this.poll() this.loopHandle = setInterval(() => this.poll, this.pollInterval) } - async stop(timeoutMs = 30000) { - const worker = this.config.worker - this.log.debug( - `Stop requested for worker '${worker}', working on job type '${this.config.type}'` - ) + /** Stops the Job Worker polling for more jobs. If await this call, and it will return as soon as all currently active jobs are completed. + * The deadline for all currently active jobs to complete is 30s by default. If the active jobs do not complete by the deadline, this method will throw. + */ + async stop(deadlineMs = 30000) { + this.log.debug(`Stop requested`, this.logMeta()) + /** Stopping polling for new jobs */ clearInterval(this.loopHandle) return new Promise((resolve, reject) => { if (this.currentlyActiveJobCount === 0) { - this.log.debug(`Worker '${worker}' - all jobs drained. Worker stopped.`) + this.log.debug(`All jobs drained. Worker stopped.`, this.logMeta()) return resolve(null) } - this.log.debug( - `Worker '${worker}' - ${this.currentlyActiveJobCount} jobs currently active.` - ) + /** This is an error timeout - if we don't complete all active jobs before the specified deadline, we reject the Promise */ const timeout = setTimeout(() => { clearInterval(wait) - return reject(`Failed to drain all jobs in ${timeoutMs}ms`) - }, timeoutMs) + this.log.debug( + `Failed to drain all jobs in ${deadlineMs}ms`, + this.logMeta() + ) + return reject(`Failed to drain all jobs in ${deadlineMs}ms`) + }, deadlineMs) + /** Check every 500ms to see if our active job count has hit zero, i.e: all active work is stopped */ const wait = setInterval(() => { if (this.currentlyActiveJobCount === 0) { - this.log.debug( - `Worker '${worker}' - all jobs drained. Worker stopped.` - ) + this.log.debug(`All jobs drained. Worker stopped.`, this.logMeta()) clearInterval(wait) clearTimeout(timeout) return resolve(null) } this.log.debug( - `Worker '${worker}' - ${this.currentlyActiveJobCount} jobs currently active.` + `Stopping - waiting for active jobs to complete.`, + this.logMeta() ) - }, 1000) + }, 500) }) } private poll() { if (this.currentlyActiveJobCount >= this.config.maxJobsToActivate) { - this.log.debug( - `Worker '${this.config.worker}' at capacity ${this.currentlyActiveJobCount} for job type '${this.config.type}'` - ) + this.log.debug(`At capacity - not requesting more jobs`, this.logMeta()) return } - this.log.silly( - `Worker: '${this.config.worker}' for job type '${this.config.type}' polling for jobs` - ) + this.log.silly(`Polling for jobs`, this.logMeta()) const remainingJobCapacity = this.config.maxJobsToActivate - this.currentlyActiveJobCount @@ -114,10 +123,8 @@ export class C8JobWorker< }) .then((jobs) => { const count = jobs.length - this.log.debug( - `Worker '${this.config.worker}' for job type '${this.config.type}' activated ${count} jobs` - ) this.currentlyActiveJobCount += count + this.log.debug(`Activated ${count} jobs`, this.logMeta()) // The job handlers for the activated jobs will run in parallel jobs.forEach((job) => this.handleJob(job)) }) @@ -128,21 +135,32 @@ export class C8JobWorker< JobCompletionInterfaceRest ) { try { - this.log.debug( - `Invoking job handler for job ${job.key} of type '${job.type}'` - ) + this.log.debug(`Invoking job handler for job ${job.key}`, this.logMeta()) await this.config.jobHandler(job, this.log) this.log.debug( - `Completed job handler for job ${job.key} of type '${job.type}'.` + `Completed job handler for job ${job.key}.`, + this.logMeta() ) } catch (e) { /** Unhandled exception in the job handler */ - this.log.error( - `Unhandled exception in job handler for job ${job.key} - Worker: ${this.config.worker} for job type: '${this.config.type}'` - ) - this.log.error(e) - this.log.error((e as Error).stack) - this.log.error(`Failing the job`) + if (e instanceof Error) { + // If err is an instance of Error, we can safely access its properties + this.log.error( + `Unhandled exception in job handler for job ${job.key}`, + this.logMeta() + ) + this.log.error(`Error: ${e.message}`, { + stack: e.stack, + ...this.logMeta(), + }) + } else { + // If err is not an Error, log it as is + this.log.error( + 'An unknown error occurred while executing a job handler', + { error: e, ...this.logMeta() } + ) + } + this.log.error(`Failing the job`, this.logMeta()) await job.fail({ errorMessage: (e as Error).toString(), retries: job.retries - 1, From 057a9feaf1a6e13011f130ca201947e39de54390 Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Wed, 18 Sep 2024 21:54:50 +1200 Subject: [PATCH 15/34] feat(camunda8): implement publishMessage over REST fixes #250 --- src/c8/lib/C8Dto.ts | 19 +++++++++++++++++++ src/c8/lib/C8RestClient.ts | 26 +++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/c8/lib/C8Dto.ts b/src/c8/lib/C8Dto.ts index 072128b8..0edb2bbc 100644 --- a/src/c8/lib/C8Dto.ts +++ b/src/c8/lib/C8Dto.ts @@ -192,3 +192,22 @@ export interface UpdateElementVariableRequest { */ operationReference?: number } + +export class CorrelateMessageResponse extends LosslessDto { + /** the unique ID of the message that was published */ + @Int64String + key!: string + /** the tenantId of the message */ + tenantId!: string + /** The key of the first process instance the message correlated with */ + @Int64String + processInstanceKey!: string +} + +export class PublishMessageResponse extends LosslessDto { + /** the unique ID of the message that was published */ + @Int64String + key!: string + /** the tenantId of the message */ + tenantId!: string +} diff --git a/src/c8/lib/C8RestClient.ts b/src/c8/lib/C8RestClient.ts index 49c03baf..e7ace190 100644 --- a/src/c8/lib/C8RestClient.ts +++ b/src/c8/lib/C8RestClient.ts @@ -35,12 +35,12 @@ import { JobCompletionInterfaceRest, JSONDoc, PublishMessageRequest, - PublishMessageResponse, TopologyResponse, } from '../../zeebe/types' import { BroadcastSignalResponse, + CorrelateMessageResponse, CreateProcessInstanceResponse, Ctor, DecisionDeployment, @@ -52,6 +52,7 @@ import { MigrationRequest, NewUserInfo, ProcessDeployment, + PublishMessageResponse, TaskChangeSet, UpdateElementVariableRequest, } from './C8Dto' @@ -256,7 +257,7 @@ export class C8RestClient { PublishMessageRequest, 'name' | 'correlationKey' | 'variables' | 'tenantId' > - ): Promise { + ) { const headers = await this.getHeaders() return this.rest.then((rest) => @@ -264,8 +265,27 @@ export class C8RestClient { .post(`messages/correlation`, { body: losslessStringify(message), headers, + parseJson: (text) => losslessParse(text, CorrelateMessageResponse), + }) + .json() + ) + } + + /** + * Publishes a single message. Messages are published to specific partitions computed from their correlation keys. + * The endpoint does not wait for a correlation result. Use `correlateMessage` for such use cases. + */ + public async publishMessage(publishMessageRequest: PublishMessageRequest) { + const headers = await this.getHeaders() + const request = this.addDefaultTenantId(publishMessageRequest) + return this.rest.then((rest) => + rest + .post(`messages/publication`, { + headers, + body: stringify(request), + parseJson: (text) => losslessParse(text, PublishMessageResponse), }) - .json() + .json() ) } From 1dcb1019b96788d952363d776a6976b99588d541 Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Wed, 18 Sep 2024 22:26:41 +1200 Subject: [PATCH 16/34] feat(camunda8): implement deleteResource over REST fixes #251 --- .../c8/rest/deleteResourceRest.spec.ts | 20 ++++++++++++ .../testdata/Delete-Resource-Rest.bpmn | 32 +++++++++++++++++++ src/c8/lib/C8Dto.ts | 3 +- src/c8/lib/C8RestClient.ts | 17 ++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/c8/rest/deleteResourceRest.spec.ts create mode 100644 src/__tests__/testdata/Delete-Resource-Rest.bpmn diff --git a/src/__tests__/c8/rest/deleteResourceRest.spec.ts b/src/__tests__/c8/rest/deleteResourceRest.spec.ts new file mode 100644 index 00000000..533680e2 --- /dev/null +++ b/src/__tests__/c8/rest/deleteResourceRest.spec.ts @@ -0,0 +1,20 @@ +import { C8RestClient } from '../../../c8/lib/C8RestClient' + +const c8 = new C8RestClient() + +test('It can delete a resource', async () => { + const res = await c8.deployResourcesFromFiles([ + './src/__tests__/testdata/Delete-Resource-Rest.bpmn', + ]) + const key = res.processes[0].processDefinitionKey + const id = res.processes[0].bpmnProcessId + const wfi = await c8.createProcessInstance({ + bpmnProcessId: id, + variables: {}, + }) + expect(wfi.processKey).toBe(key) + await c8.deleteResource({ resourceKey: key }) + await expect( + c8.createProcessInstance({ bpmnProcessId: id, variables: {} }) + ).rejects.toThrow() +}) diff --git a/src/__tests__/testdata/Delete-Resource-Rest.bpmn b/src/__tests__/testdata/Delete-Resource-Rest.bpmn new file mode 100644 index 00000000..00b396ae --- /dev/null +++ b/src/__tests__/testdata/Delete-Resource-Rest.bpmn @@ -0,0 +1,32 @@ + + + + + Flow_0z9jd9c + + + Flow_0z9jd9c + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/c8/lib/C8Dto.ts b/src/c8/lib/C8Dto.ts index 0edb2bbc..c97d1f11 100644 --- a/src/c8/lib/C8Dto.ts +++ b/src/c8/lib/C8Dto.ts @@ -125,7 +125,8 @@ export class CreateProcessInstanceResponse extends LosslessDto { * The unique key identifying the process definition (e.g. returned from a process * in the DeployResourceResponse message) */ - readonly processDefinitionKey!: string + @Int64String + readonly processKey!: string /** * The BPMN process ID of the process definition */ diff --git a/src/c8/lib/C8RestClient.ts b/src/c8/lib/C8RestClient.ts index e7ace190..ad2b9c50 100644 --- a/src/c8/lib/C8RestClient.ts +++ b/src/c8/lib/C8RestClient.ts @@ -639,6 +639,23 @@ export class C8RestClient { return this.deployResources(resources) } + /** + * 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. + */ + public async deleteResource(req: { + resourceKey: string + operationReference?: number + }) { + const headers = await this.getHeaders() + const { resourceKey, operationReference } = req + return this.rest.then((rest) => + rest.post(`resources/${resourceKey}/deletion`, { + headers, + body: 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. From ff248b716320b4ca945d768320ffcf658a597861 Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Thu, 19 Sep 2024 11:34:46 +1200 Subject: [PATCH 17/34] test(optimize): comment out test for Self-Managed This relates to #253 --- .../optimize/optimize.integration.spec.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/__tests__/optimize/optimize.integration.spec.ts b/src/__tests__/optimize/optimize.integration.spec.ts index fceea2ce..fda62f2a 100644 --- a/src/__tests__/optimize/optimize.integration.spec.ts +++ b/src/__tests__/optimize/optimize.integration.spec.ts @@ -1,16 +1,15 @@ -import { promises as fsPromises } from 'fs' - import { OptimizeApiClient } from '../../optimize/lib/OptimizeApiClient' +/** + * Automatically spun up environments for testing do not have data in them for read operations from Optimize. + * So this test 404s as expected. + * It is testing that we can auth correctly, and access the endpoint. + */ +// Test disabled. See: https://github.com/camunda/camunda-8-js-sdk/issues/253 xtest('Can get Dashboards', async () => { const id = '8a7103a7-c086-48f8-b5b7-a7f83e864688' const client = new OptimizeApiClient() - const res = await client.exportDashboardDefinitions([id]) - await fsPromises.writeFile( - 'exported-dashboard.json', - JSON.stringify(res, null, 2) - ) - expect(res).toBeTruthy() + await expect(client.exportDashboardDefinitions([id])).rejects.toThrow('404') }) test('Can get readiness', async () => { From 4ec4fa1b6c1554bbac04ba275da3e1bba2dbf012 Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Thu, 19 Sep 2024 17:00:12 +1200 Subject: [PATCH 18/34] feat(camunda8): implement createProcessInstanceWithResult --- .../c8/rest/createProcessRest.spec.ts | 90 +++++++++++++++++++ .../testdata/create-process-rest.bpmn | 32 +++++++ src/c8/lib/C8Dto.ts | 70 ++++++++++++++- src/c8/lib/C8RestClient.ts | 70 +++++++++++++-- .../lib/RestApiProcessInstanceClassFactory.ts | 55 ++++++++++++ 5 files changed, 311 insertions(+), 6 deletions(-) create mode 100644 src/__tests__/c8/rest/createProcessRest.spec.ts create mode 100644 src/__tests__/testdata/create-process-rest.bpmn create mode 100644 src/c8/lib/RestApiProcessInstanceClassFactory.ts diff --git a/src/__tests__/c8/rest/createProcessRest.spec.ts b/src/__tests__/c8/rest/createProcessRest.spec.ts new file mode 100644 index 00000000..65625ce9 --- /dev/null +++ b/src/__tests__/c8/rest/createProcessRest.spec.ts @@ -0,0 +1,90 @@ +import path from 'node:path' + +import { C8RestClient } from '../../../c8/lib/C8RestClient' +import { LosslessDto } from '../../../lib' + +let bpmnProcessId: string +let processDefinitionKey: string +const restClient = new C8RestClient() + +beforeAll(async () => { + const res = await restClient.deployResourcesFromFiles([ + path.join('.', 'src', '__tests__', 'testdata', 'create-process-rest.bpmn'), + ]) + ;({ bpmnProcessId, processDefinitionKey } = res.processes[0]) +}) + +class myVariableDto extends LosslessDto { + someNumberField?: number +} + +test('Can create a process from bpmn id', (done) => { + restClient + .createProcessInstance({ + bpmnProcessId, + variables: { + someNumberField: 8, + }, + }) + .then((res) => { + expect(res.processKey).toEqual(processDefinitionKey) + done() + }) +}) + +test('Can create a process from process definition key', (done) => { + restClient + .createProcessInstance({ + processDefinitionKey, + variables: { + someNumberField: 8, + }, + }) + .then((res) => { + expect(res.processKey).toEqual(processDefinitionKey) + done() + }) +}) + +test('Can create a process with a lossless Dto', (done) => { + restClient + .createProcessInstance({ + processDefinitionKey, + variables: new myVariableDto({ someNumberField: 8 }), + }) + .then((res) => { + expect(res.processKey).toEqual(processDefinitionKey) + done() + }) +}) + +test('Can create a process and get the result', (done) => { + const variables = new myVariableDto({ someNumberField: 8 }) + restClient + .createProcessInstanceWithResult({ + processDefinitionKey, + variables, + outputVariablesDto: myVariableDto, + }) + .then((res) => { + console.log(res) + expect(res.processKey).toEqual(processDefinitionKey) + expect(res.variables.someNumberField).toBe(8) + done() + }) +}) + +test('Can create a process and get the result', (done) => { + restClient + .createProcessInstanceWithResult({ + processDefinitionKey, + variables: new myVariableDto({ someNumberField: 9 }), + }) + .then((res) => { + expect(res.processKey).toEqual(processDefinitionKey) + // Without an outputVariablesDto, the response variables will be of type unknown + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((res.variables as any).someNumberField).toBe(9) + done() + }) +}) diff --git a/src/__tests__/testdata/create-process-rest.bpmn b/src/__tests__/testdata/create-process-rest.bpmn new file mode 100644 index 00000000..4f6d520e --- /dev/null +++ b/src/__tests__/testdata/create-process-rest.bpmn @@ -0,0 +1,32 @@ + + + + + Flow_15yxzfg + + + Flow_15yxzfg + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/c8/lib/C8Dto.ts b/src/c8/lib/C8Dto.ts index c97d1f11..af234b35 100644 --- a/src/c8/lib/C8Dto.ts +++ b/src/c8/lib/C8Dto.ts @@ -120,7 +120,7 @@ export class DeployResourceResponse extends DeployResourceResponseDto { forms!: FormDeployment[] } -export class CreateProcessInstanceResponse extends LosslessDto { +export class CreateProcessInstanceResponse> { /** * The unique key identifying the process definition (e.g. returned from a process * in the DeployResourceResponse message) @@ -141,6 +141,10 @@ export class CreateProcessInstanceResponse extends LosslessDto { * the tenant identifier of the created process instance */ readonly tenantId!: string + /** + * If `awaitCompletion` is true, this will be populated with the output variables. Otherwise, it will be an empty object. + */ + readonly variables!: T } export interface MigrationMappingInstruction { @@ -212,3 +216,67 @@ export class PublishMessageResponse extends LosslessDto { /** the tenantId of the message */ tenantId!: string } + +export interface CreateProcessBaseRequest { + /** + * the version of the process; if not specified it will use the latest version + */ + version?: number + /** + * JSON document that will instantiate the variables for the root variable scope of the + * process instance. + */ + variables: V + /** The tenantId for a multi-tenant enabled cluster. */ + tenantId?: string + /** a reference key chosen by the user and will be part of all records resulted from this operation */ + operationReference?: number | LosslessNumber + /** + * List of start instructions. If empty (default) the process instance + * will start at the start event. If non-empty the process instance will apply start + * instructions after it has been created + */ + startInstructions?: ProcessInstanceCreationStartInstruction[] + /** + * Wait for the process instance to complete. If the process instance completion does not occur within the requestTimeout, the request will be closed. Defaults to false. + */ + // This is commented out, because we used specialised methods for the two cases. + // awaitCompletion?: boolean + /** + * Timeout (in ms) the request waits for the process to complete. By default or when set to 0, the generic request timeout configured in the cluster is applied. + */ + requestTimeout?: number +} + +export interface ProcessInstanceCreationStartInstruction { + /** + * future extensions might include + * - different types of start instructions + * - ability to set local variables for different flow scopes + * for now, however, the start instruction is implicitly a + * "startBeforeElement" instruction + */ + elementId: string +} + +export interface CreateProcessInstanceFromBpmnProcessId< + V extends JSONDoc | LosslessDto, +> extends CreateProcessBaseRequest { + /** + * the BPMN process ID of the process definition + */ + bpmnProcessId: string +} + +export interface CreateProcessInstanceFromProcessDefinition< + V extends JSONDoc | LosslessDto, +> extends CreateProcessBaseRequest { + /** + * the key of the process definition + */ + processDefinitionKey: string +} + +export type CreateProcessInstanceReq = + | CreateProcessInstanceFromBpmnProcessId + | CreateProcessInstanceFromProcessDefinition diff --git a/src/c8/lib/C8RestClient.ts b/src/c8/lib/C8RestClient.ts index ad2b9c50..02b21a50 100644 --- a/src/c8/lib/C8RestClient.ts +++ b/src/c8/lib/C8RestClient.ts @@ -26,7 +26,6 @@ import { ActivateJobsRequest, BroadcastSignalReq, CompleteJobRequest, - CreateProcessInstanceReq, ErrorJobWithVariables, FailJobRequest, IProcessVariables, @@ -41,6 +40,7 @@ import { import { BroadcastSignalResponse, CorrelateMessageResponse, + CreateProcessInstanceReq, CreateProcessInstanceResponse, Ctor, DecisionDeployment, @@ -59,11 +59,14 @@ import { import { C8JobWorker, C8JobWorkerConfig } from './C8JobWorker' import { getLogger } from './C8Logger' import { createSpecializedRestApiJobClass } from './RestApiJobClassFactory' +import { createSpecializedCreateProcessInstanceResponseClass } from './RestApiProcessInstanceClassFactory' const trace = debug('camunda:zeebe') const CAMUNDA_REST_API_VERSION = 'v2' +class DefaultLosslessDto extends LosslessDto {} + export class C8RestClient { private userAgentString: string private oAuthProvider: IOAuthProvider @@ -448,25 +451,82 @@ export class C8RestClient { } /** - * Create and start a process instance + * Create and start a process instance. This method does not await the outcome of the process. For that, use `createProcessInstanceWithResult`. */ - public async createProcessInstance( + public async createProcessInstance( request: CreateProcessInstanceReq + ): Promise>> + + async createProcessInstance< + T extends JSONDoc | LosslessDto, + V extends LosslessDto, + >( + request: CreateProcessInstanceReq & { + outputVariablesDto?: Ctor + } ) { const headers = await this.getHeaders() + const outputVariablesDto: Ctor | Ctor = + (request.outputVariablesDto as Ctor) ?? DefaultLosslessDto + + const CreateProcessInstanceResponseWithVariablesDto = + createSpecializedCreateProcessInstanceResponseClass(outputVariablesDto) + return this.rest.then((rest) => rest .post(`process-instances`, { body: losslessStringify(this.addDefaultTenantId(request)), headers, parseJson: (text) => - losslessParse(text, CreateProcessInstanceResponse), + losslessParse(text, CreateProcessInstanceResponseWithVariablesDto), }) - .json() + .json< + InstanceType + >() ) } + /** + * Create and start a process instance. This method awaits the outcome of the process. + */ + public async createProcessInstanceWithResult( + request: CreateProcessInstanceReq + ): Promise> + + public async createProcessInstanceWithResult< + T extends JSONDoc | LosslessDto, + V extends LosslessDto, + >( + request: CreateProcessInstanceReq & { + outputVariablesDto: Ctor + } + ): Promise> + public async createProcessInstanceWithResult< + T extends JSONDoc | LosslessDto, + V, + >( + request: CreateProcessInstanceReq & { + outputVariablesDto?: Ctor + } + ) { + /** + * 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, + } as unknown as CreateProcessInstanceReq) + } + /** * Cancel an active process instance */ diff --git a/src/c8/lib/RestApiProcessInstanceClassFactory.ts b/src/c8/lib/RestApiProcessInstanceClassFactory.ts new file mode 100644 index 00000000..d5f1654d --- /dev/null +++ b/src/c8/lib/RestApiProcessInstanceClassFactory.ts @@ -0,0 +1,55 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { LosslessDto } from '../../lib' + +import { CreateProcessInstanceResponse } from './C8Dto' + +const factory = + createMemoizedSpecializedCreateProcessInstanceResponseClassFactory() + +// Creates a specialized RestApiJob class that is cached based on output variables +export const createSpecializedCreateProcessInstanceResponseClass = < + Variables extends LosslessDto, +>( + outputVariableDto: new (obj: any) => Variables +) => { + return factory(outputVariableDto) +} + +function createMemoizedSpecializedCreateProcessInstanceResponseClassFactory() { + const cache = new Map() + + return function ( + outputVariableDto: new (obj: any) => Variables + ): new (obj: any) => CreateProcessInstanceResponse { + // Create a unique cache key based on the class and inputs + const cacheKey = JSON.stringify({ + outputVariableDto, + }) + + // Check for cached result + if (cache.has(cacheKey)) { + return cache.get(cacheKey) + } + + // Create a new class that extends the original class + class CustomCreateProcessInstanceResponseClass< + Variables extends LosslessDto, + > extends CreateProcessInstanceResponse { + variables!: Variables + } + + // Use Reflect to define the metadata on the new class's prototype + Reflect.defineMetadata( + 'child:class', + outputVariableDto, + CustomCreateProcessInstanceResponseClass.prototype, + 'variables' + ) + + // Store the new class in cache + cache.set(cacheKey, CustomCreateProcessInstanceResponseClass) + + // Return the new class + return CustomCreateProcessInstanceResponseClass + } +} From bb5d8ea666d7d6f0da9823f7ecf0a187708eab6b Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Thu, 19 Sep 2024 20:30:50 +1200 Subject: [PATCH 19/34] fix(lossless-parser): throw on encountering Date, Map, or Set fixes #254 --- .../lib/LosslessJsonParser.unit.spec.ts | 14 ++++++++++++++ src/lib/LosslessJsonParser.ts | 16 ++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/__tests__/lib/LosslessJsonParser.unit.spec.ts b/src/__tests__/lib/LosslessJsonParser.unit.spec.ts index 253d3eb1..25f58b4f 100644 --- a/src/__tests__/lib/LosslessJsonParser.unit.spec.ts +++ b/src/__tests__/lib/LosslessJsonParser.unit.spec.ts @@ -384,3 +384,17 @@ test('LosslessJsonParser will throw if given stringified JSON with an unsafe int expect(threw).toBe(true) }) + +test('It rejects Date, Map, and Set types', () => { + class Dto extends LosslessDto { + date!: Date + name!: string + } + const date = new Date() + const dto = new Dto({ date, name: 'me' }) + expect(() => losslessStringify(dto)).toThrow('Date') + const mapDto = new Dto({ map: new Map() }) + expect(() => losslessStringify(mapDto)).toThrow('Map') + const setDto = new Dto({ set: new Set() }) + expect(() => losslessStringify(setDto)).toThrow('Set') +}) diff --git a/src/lib/LosslessJsonParser.ts b/src/lib/LosslessJsonParser.ts index c4d56c7c..39377bf5 100644 --- a/src/lib/LosslessJsonParser.ts +++ b/src/lib/LosslessJsonParser.ts @@ -308,6 +308,22 @@ export function losslessStringify( debug(`Object is not a LosslessDto. Stringifying as normal JSON.`) } + if (obj instanceof Date) { + throw new Error( + `Date type not supported in variables. Please serialize with .toISOString() before passing to Camunda` + ) + } + if (obj instanceof Map) { + throw new Error( + `Map type not supported in variables. Please serialize with Object.fromEntries() before passing to Camunda` + ) + } + if (obj instanceof Set) { + throw new Error( + `Set type not supported in variables. Please serialize with Array.from() before passing to Camunda` + ) + } + const newObj: any = Array.isArray(obj) ? [] : {} Object.keys(obj).forEach((key) => { From 0d97f681ad1a1c9d295f43c83f342b6eb5cfa3da Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Fri, 20 Sep 2024 16:10:14 +1200 Subject: [PATCH 20/34] feat(camunda8): add modifyAuthorization method --- src/c8/lib/C8Dto.ts | 29 ++++++++++++ src/c8/lib/C8RestClient.ts | 90 ++++++++++++++++++++++++++------------ 2 files changed, 91 insertions(+), 28 deletions(-) diff --git a/src/c8/lib/C8Dto.ts b/src/c8/lib/C8Dto.ts index af234b35..d671892c 100644 --- a/src/c8/lib/C8Dto.ts +++ b/src/c8/lib/C8Dto.ts @@ -280,3 +280,32 @@ export interface CreateProcessInstanceFromProcessDefinition< export type CreateProcessInstanceReq = | CreateProcessInstanceFromBpmnProcessId | CreateProcessInstanceFromProcessDefinition + +export interface PatchAuthorizationRequest { + /** The key of the owner of the authorization. */ + ownerKey: string + /** Indicates if permissions should be added or removed. */ + action: 'ADD' | 'REMOVE' + /** The type of resource to add/remove perissions to/from. */ + resourceType: + | 'AUTHORIZATION' + | 'MESSAGE' + | 'JOB' + | 'APPLICATION' + | 'TENANT' + | 'DEPLOYMENT' + | 'PROCESS_DEFINITION' + | 'USER_TASK' + | 'DECISION_REQUIREMENTS_DEFINITION' + | 'DECISION_DEFINITION' + | 'USER_GROUP' + | 'USER' + | 'ROLE' + /** The permissions to add/remove. */ + permissions: { + /** Specifies the type of permissions. */ + permissionType: 'CREATE' | 'READ' | 'UPDATE' | 'DELETE' + /** A list of resource IDs the permission relates to. */ + resourceIds: [] + }[] +} diff --git a/src/c8/lib/C8RestClient.ts b/src/c8/lib/C8RestClient.ts index 02b21a50..6788ed35 100644 --- a/src/c8/lib/C8RestClient.ts +++ b/src/c8/lib/C8RestClient.ts @@ -51,6 +51,7 @@ import { JobUpdateChangeset, MigrationRequest, NewUserInfo, + PatchAuthorizationRequest, ProcessDeployment, PublishMessageResponse, TaskChangeSet, @@ -131,6 +132,22 @@ export class C8RestClient { return headers } + /** + * Manage the permissions assigned to authorization. + */ + public async modifyAuthorization(req: PatchAuthorizationRequest) { + const headers = await this.getHeaders() + const { ownerKey, ...request } = req + return this.rest.then((rest) => + rest + .patch(`authorizations/${ownerKey}`, { + headers, + body: stringify(request), + }) + .json() + ) + } + /** * Broadcasts a signal. */ @@ -169,13 +186,15 @@ export class C8RestClient { }) { const headers = await this.getHeaders() return this.rest.then((rest) => - rest.post(`user-tasks/${userTaskKey}/completion`, { - body: losslessStringify({ - variables, - action, - }), - headers, - }) + rest + .post(`user-tasks/${userTaskKey}/completion`, { + body: losslessStringify({ + variables, + action, + }), + headers, + }) + .json() ) } @@ -194,14 +213,16 @@ export class C8RestClient { const headers = await this.getHeaders() return this.rest.then((rest) => - rest.post(`user-tasks/${userTaskKey}/assignment`, { - body: losslessStringify({ - allowOverride, - action, - assignee, - }), - headers, - }) + rest + .post(`user-tasks/${userTaskKey}/assignment`, { + body: losslessStringify({ + allowOverride, + action, + assignee, + }), + headers, + }) + .json() ) } @@ -216,10 +237,12 @@ export class C8RestClient { const headers = await this.getHeaders() return this.rest.then((rest) => - rest.patch(`user-tasks/${userTaskKey}/update`, { - body: losslessStringify(changeset), - headers, - }) + rest + .patch(`user-tasks/${userTaskKey}/update`, { + body: losslessStringify(changeset), + headers, + }) + .json() ) } /* Removes the assignee of a task with the given key. */ @@ -227,7 +250,7 @@ export class C8RestClient { const headers = await this.getHeaders() return this.rest.then((rest) => - rest.delete(`user-tasks/${userTaskKey}/assignee`, { headers }) + rest.delete(`user-tasks/${userTaskKey}/assignee`, { headers }).json() ) } @@ -238,10 +261,12 @@ export class C8RestClient { const headers = await this.getHeaders() return this.rest.then((rest) => - rest.post(`users`, { - body: JSON.stringify(newUserInfo), - headers, - }) + rest + .post(`users`, { + body: JSON.stringify(newUserInfo), + headers, + }) + .json() ) } @@ -262,11 +287,12 @@ export class C8RestClient { > ) { const headers = await this.getHeaders() + const body = losslessStringify(this.addDefaultTenantId(message)) return this.rest.then((rest) => rest .post(`messages/correlation`, { - body: losslessStringify(message), + body, headers, parseJson: (text) => losslessParse(text, CorrelateMessageResponse), }) @@ -280,12 +306,14 @@ export class C8RestClient { */ public async publishMessage(publishMessageRequest: PublishMessageRequest) { const headers = await this.getHeaders() - const request = this.addDefaultTenantId(publishMessageRequest) + const body = losslessStringify( + this.addDefaultTenantId(publishMessageRequest) + ) return this.rest.then((rest) => rest .post(`messages/publication`, { headers, - body: stringify(request), + body, parseJson: (text) => losslessParse(text, PublishMessageResponse), }) .json() @@ -491,7 +519,10 @@ export class C8RestClient { * Create and start a process instance. This method awaits the outcome of the process. */ public async createProcessInstanceWithResult( - request: CreateProcessInstanceReq + request: CreateProcessInstanceReq & { + /** An array of variable names to fetch. If not supplied, all visible variables in the root scope will be returned */ + fetchVariables?: string[] + } ): Promise> public async createProcessInstanceWithResult< @@ -499,6 +530,9 @@ export class C8RestClient { V extends LosslessDto, >( request: CreateProcessInstanceReq & { + /** An array of variable names to fetch. If not supplied, all visible variables in the root scope will be returned */ + fetchVariables?: string[] + /** A Dto specifying the shape of the output variables. If not supplied, the output variables will be returned as a `LosslessDto` of type `unknown`. */ outputVariablesDto: Ctor } ): Promise> From 20f40f21e809bfea3e6bbe1e63cb767bed76e48b Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Fri, 20 Sep 2024 16:10:58 +1200 Subject: [PATCH 21/34] test(camunda8): update tests --- ...Jobs.spec.ts => activateJobs.rest.spec.ts} | 10 +- ...al.spec.ts => broadcastSigna.rest.spec.ts} | 0 ...est.spec.ts => createProcess.rest.spec.ts} | 18 +++- ...st.spec.ts => deleteResource.rest.spec.ts} | 3 +- ...ss.spec.ts => migrateProcess.rest.spec.ts} | 3 +- ...pinClock.spec.ts => pinClock.rest.spec.ts} | 0 .../c8/rest/publishMessage.rest.spec.ts | 95 +++++++++++++++++++ .../testdata/hello-world-complete-rest.bpmn | 55 +++++++++++ src/__tests__/testdata/rest-message-test.bpmn | 53 +++++++++++ src/lib/GotHooks.ts | 8 +- 10 files changed, 237 insertions(+), 8 deletions(-) rename src/__tests__/c8/rest/{activateJobs.spec.ts => activateJobs.rest.spec.ts} (83%) rename src/__tests__/c8/rest/{restBroadcastSignal.spec.ts => broadcastSigna.rest.spec.ts} (100%) rename src/__tests__/c8/rest/{createProcessRest.spec.ts => createProcess.rest.spec.ts} (80%) rename src/__tests__/c8/rest/{deleteResourceRest.spec.ts => deleteResource.rest.spec.ts} (82%) rename src/__tests__/c8/rest/{restMigrateProcess.spec.ts => migrateProcess.rest.spec.ts} (98%) rename src/__tests__/c8/rest/{pinClock.spec.ts => pinClock.rest.spec.ts} (100%) create mode 100644 src/__tests__/c8/rest/publishMessage.rest.spec.ts create mode 100644 src/__tests__/testdata/hello-world-complete-rest.bpmn create mode 100644 src/__tests__/testdata/rest-message-test.bpmn diff --git a/src/__tests__/c8/rest/activateJobs.spec.ts b/src/__tests__/c8/rest/activateJobs.rest.spec.ts similarity index 83% rename from src/__tests__/c8/rest/activateJobs.spec.ts rename to src/__tests__/c8/rest/activateJobs.rest.spec.ts index 8862d825..c5918829 100644 --- a/src/__tests__/c8/rest/activateJobs.spec.ts +++ b/src/__tests__/c8/rest/activateJobs.rest.spec.ts @@ -7,7 +7,13 @@ const restClient = new C8RestClient() beforeAll(async () => { const res = await restClient.deployResourcesFromFiles([ - path.join('.', 'src', '__tests__', 'testdata', 'hello-world-complete.bpmn'), + path.join( + '.', + 'src', + '__tests__', + 'testdata', + 'hello-world-complete-rest.bpmn' + ), ]) bpmnProcessId = res.processes[0].bpmnProcessId }) @@ -26,7 +32,7 @@ test('Can service a task', (done) => { maxJobsToActivate: 2, requestTimeout: 5000, timeout: 5000, - type: 'console-log-complete', + type: 'console-log-complete-rest', worker: 'test', }) .then((jobs) => { diff --git a/src/__tests__/c8/rest/restBroadcastSignal.spec.ts b/src/__tests__/c8/rest/broadcastSigna.rest.spec.ts similarity index 100% rename from src/__tests__/c8/rest/restBroadcastSignal.spec.ts rename to src/__tests__/c8/rest/broadcastSigna.rest.spec.ts diff --git a/src/__tests__/c8/rest/createProcessRest.spec.ts b/src/__tests__/c8/rest/createProcess.rest.spec.ts similarity index 80% rename from src/__tests__/c8/rest/createProcessRest.spec.ts rename to src/__tests__/c8/rest/createProcess.rest.spec.ts index 65625ce9..565c7d58 100644 --- a/src/__tests__/c8/rest/createProcessRest.spec.ts +++ b/src/__tests__/c8/rest/createProcess.rest.spec.ts @@ -3,6 +3,8 @@ import path from 'node:path' import { C8RestClient } from '../../../c8/lib/C8RestClient' import { LosslessDto } from '../../../lib' +jest.setTimeout(30000) + let bpmnProcessId: string let processDefinitionKey: string const restClient = new C8RestClient() @@ -67,7 +69,6 @@ test('Can create a process and get the result', (done) => { outputVariablesDto: myVariableDto, }) .then((res) => { - console.log(res) expect(res.processKey).toEqual(processDefinitionKey) expect(res.variables.someNumberField).toBe(8) done() @@ -88,3 +89,18 @@ test('Can create a process and get the result', (done) => { done() }) }) + +test('What happens if we time out?', async () => { + const res = await restClient.deployResourcesFromFiles([ + path.join('.', 'src', '__tests__', 'testdata', 'hello-world-complete.bpmn'), + ]) + const bpmnProcessId = res.processes[0].bpmnProcessId + // @TODO: we should get a 504 Gateway Timeout for this, not a 500 + await expect( + restClient.createProcessInstanceWithResult({ + bpmnProcessId, + variables: new myVariableDto({ someNumberField: 9 }), + requestTimeout: 20000, + }) + ).rejects.toThrow('500') +}) diff --git a/src/__tests__/c8/rest/deleteResourceRest.spec.ts b/src/__tests__/c8/rest/deleteResource.rest.spec.ts similarity index 82% rename from src/__tests__/c8/rest/deleteResourceRest.spec.ts rename to src/__tests__/c8/rest/deleteResource.rest.spec.ts index 533680e2..f9540101 100644 --- a/src/__tests__/c8/rest/deleteResourceRest.spec.ts +++ b/src/__tests__/c8/rest/deleteResource.rest.spec.ts @@ -14,7 +14,8 @@ test('It can delete a resource', async () => { }) expect(wfi.processKey).toBe(key) await c8.deleteResource({ resourceKey: key }) + // After deleting the process definition, we should not be able to start a new process instance. await expect( c8.createProcessInstance({ bpmnProcessId: id, variables: {} }) - ).rejects.toThrow() + ).rejects.toThrow('404') }) diff --git a/src/__tests__/c8/rest/restMigrateProcess.spec.ts b/src/__tests__/c8/rest/migrateProcess.rest.spec.ts similarity index 98% rename from src/__tests__/c8/rest/restMigrateProcess.spec.ts rename to src/__tests__/c8/rest/migrateProcess.rest.spec.ts index fdfff7c6..6be1d4c8 100644 --- a/src/__tests__/c8/rest/restMigrateProcess.spec.ts +++ b/src/__tests__/c8/rest/migrateProcess.rest.spec.ts @@ -3,8 +3,7 @@ import path from 'path' import { C8JobWorker } from 'c8/lib/C8JobWorker' import { C8RestClient } from '../../../c8/lib/C8RestClient' - -import { LosslessDto } from './../../../lib' +import { LosslessDto } from '../../../lib' const c8 = new C8RestClient() diff --git a/src/__tests__/c8/rest/pinClock.spec.ts b/src/__tests__/c8/rest/pinClock.rest.spec.ts similarity index 100% rename from src/__tests__/c8/rest/pinClock.spec.ts rename to src/__tests__/c8/rest/pinClock.rest.spec.ts diff --git a/src/__tests__/c8/rest/publishMessage.rest.spec.ts b/src/__tests__/c8/rest/publishMessage.rest.spec.ts new file mode 100644 index 00000000..b56b98be --- /dev/null +++ b/src/__tests__/c8/rest/publishMessage.rest.spec.ts @@ -0,0 +1,95 @@ +import { v4 } from 'uuid' + +import { C8RestClient } from '../../../c8/lib/C8RestClient' +import { LosslessDto } from '../../../lib' + +const c8 = new C8RestClient() + +beforeAll(async () => { + await c8.deployResourcesFromFiles([ + './src/__tests__/testdata/rest-message-test.bpmn', + ]) +}) + +test('Can publish a message', (done) => { + const uuid = v4() + const outputVariablesDto = class extends LosslessDto { + messageReceived!: boolean + } + c8.createProcessInstanceWithResult({ + bpmnProcessId: 'rest-message-test', + variables: { + correlationId: uuid, + }, + outputVariablesDto, + }).then((result) => { + expect(result.variables.messageReceived).toBe(true) + done() + }) + c8.publishMessage({ + correlationKey: uuid, + messageId: uuid, + name: 'rest-message', + variables: { + messageReceived: true, + }, + timeToLive: 10000, + }) +}) + +test('Can correlate a message', (done) => { + const uuid = v4() + const outputVariablesDto = class extends LosslessDto { + messageReceived!: boolean + } + c8.createProcessInstanceWithResult({ + bpmnProcessId: 'rest-message-test', + variables: { + correlationId: uuid, + }, + outputVariablesDto, + }).then((result) => { + expect(result.variables.messageReceived).toBe(true) + done() + }) + setTimeout( + () => + c8.correlateMessage({ + correlationKey: uuid, + name: 'rest-message', + variables: { + messageReceived: true, + }, + }), + 1000 + ) +}) + +test('Correlate message returns expected data', (done) => { + const uuid = v4() + let processInstanceKey: string + c8.createProcessInstance({ + bpmnProcessId: 'rest-message-test', + variables: { + correlationId: uuid, + }, + }).then((result) => { + processInstanceKey = result.processInstanceKey + setTimeout( + () => + c8 + .correlateMessage({ + correlationKey: uuid, + name: 'rest-message', + variables: { + messageReceived: true, + }, + }) + .then((res) => { + expect(res.processInstanceKey).toBe(processInstanceKey) + done() + }), + 1000 + ) + }) +}) diff --git a/src/__tests__/testdata/hello-world-complete-rest.bpmn b/src/__tests__/testdata/hello-world-complete-rest.bpmn new file mode 100644 index 00000000..263f15cc --- /dev/null +++ b/src/__tests__/testdata/hello-world-complete-rest.bpmn @@ -0,0 +1,55 @@ + + + + + SequenceFlow_0fp53hs + + + + + + + + + SequenceFlow_0fp53hs + SequenceFlow_112zghv + + + + SequenceFlow_112zghv + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/__tests__/testdata/rest-message-test.bpmn b/src/__tests__/testdata/rest-message-test.bpmn new file mode 100644 index 00000000..fca878af --- /dev/null +++ b/src/__tests__/testdata/rest-message-test.bpmn @@ -0,0 +1,53 @@ + + + + + Flow_083hybf + + + + Flow_083hybf + Flow_0c1qlsc + + + + Flow_0c1qlsc + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/lib/GotHooks.ts b/src/lib/GotHooks.ts index 2f3ca71d..b9159199 100644 --- a/src/lib/GotHooks.ts +++ b/src/lib/GotHooks.ts @@ -22,18 +22,22 @@ export const gotErrorHandler = (options, next) => { */ export const gotBeforeErrorHook = (error) => { const { request } = error + let detail = '' if (error instanceof GotHTTPError) { error = new HTTPError(error.response) try { - const details = JSON.parse((error.response?.body as string) || '{}') + const details = JSON.parse( + (error.response?.body as string) || '{detail:""}' + ) error.statusCode = details.status + detail = details.detail ?? '' } catch (e) { error.statusCode = 0 } } // eslint-disable-next-line @typescript-eslint/no-explicit-any ;(error as any).source = (error as any).options.context.stack.split('\n') - error.message += ` (request to ${request?.options.url.href})` + error.message += ` (request to ${request?.options.url.href}). ${detail}` return error } From b4545b7882060af0cedd4e637fa84ca76c1aa7d0 Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Mon, 23 Sep 2024 11:31:24 +1200 Subject: [PATCH 22/34] test(camunda8): align with createProcessInstance endpoint 504 on timeout, processDefinitionKey --- src/__tests__/c8/rest/createProcess.rest.spec.ts | 15 +++++++-------- src/__tests__/c8/rest/deleteResource.rest.spec.ts | 2 +- src/c8/lib/C8Dto.ts | 2 +- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/__tests__/c8/rest/createProcess.rest.spec.ts b/src/__tests__/c8/rest/createProcess.rest.spec.ts index 565c7d58..e0e2c62d 100644 --- a/src/__tests__/c8/rest/createProcess.rest.spec.ts +++ b/src/__tests__/c8/rest/createProcess.rest.spec.ts @@ -3,7 +3,7 @@ import path from 'node:path' import { C8RestClient } from '../../../c8/lib/C8RestClient' import { LosslessDto } from '../../../lib' -jest.setTimeout(30000) +jest.setTimeout(17000) let bpmnProcessId: string let processDefinitionKey: string @@ -29,7 +29,7 @@ test('Can create a process from bpmn id', (done) => { }, }) .then((res) => { - expect(res.processKey).toEqual(processDefinitionKey) + expect(res.processDefinitionKey).toEqual(processDefinitionKey) done() }) }) @@ -43,7 +43,7 @@ test('Can create a process from process definition key', (done) => { }, }) .then((res) => { - expect(res.processKey).toEqual(processDefinitionKey) + expect(res.processDefinitionKey).toEqual(processDefinitionKey) done() }) }) @@ -55,7 +55,7 @@ test('Can create a process with a lossless Dto', (done) => { variables: new myVariableDto({ someNumberField: 8 }), }) .then((res) => { - expect(res.processKey).toEqual(processDefinitionKey) + expect(res.processDefinitionKey).toEqual(processDefinitionKey) done() }) }) @@ -69,7 +69,7 @@ test('Can create a process and get the result', (done) => { outputVariablesDto: myVariableDto, }) .then((res) => { - expect(res.processKey).toEqual(processDefinitionKey) + expect(res.processDefinitionKey).toEqual(processDefinitionKey) expect(res.variables.someNumberField).toBe(8) done() }) @@ -82,7 +82,7 @@ test('Can create a process and get the result', (done) => { variables: new myVariableDto({ someNumberField: 9 }), }) .then((res) => { - expect(res.processKey).toEqual(processDefinitionKey) + expect(res.processDefinitionKey).toEqual(processDefinitionKey) // Without an outputVariablesDto, the response variables will be of type unknown // eslint-disable-next-line @typescript-eslint/no-explicit-any expect((res.variables as any).someNumberField).toBe(9) @@ -95,12 +95,11 @@ test('What happens if we time out?', async () => { path.join('.', 'src', '__tests__', 'testdata', 'hello-world-complete.bpmn'), ]) const bpmnProcessId = res.processes[0].bpmnProcessId - // @TODO: we should get a 504 Gateway Timeout for this, not a 500 await expect( restClient.createProcessInstanceWithResult({ bpmnProcessId, variables: new myVariableDto({ someNumberField: 9 }), requestTimeout: 20000, }) - ).rejects.toThrow('500') + ).rejects.toThrow('504') }) diff --git a/src/__tests__/c8/rest/deleteResource.rest.spec.ts b/src/__tests__/c8/rest/deleteResource.rest.spec.ts index f9540101..6dd6b298 100644 --- a/src/__tests__/c8/rest/deleteResource.rest.spec.ts +++ b/src/__tests__/c8/rest/deleteResource.rest.spec.ts @@ -12,7 +12,7 @@ test('It can delete a resource', async () => { bpmnProcessId: id, variables: {}, }) - expect(wfi.processKey).toBe(key) + expect(wfi.processDefinitionKey).toBe(key) await c8.deleteResource({ resourceKey: key }) // After deleting the process definition, we should not be able to start a new process instance. await expect( diff --git a/src/c8/lib/C8Dto.ts b/src/c8/lib/C8Dto.ts index d671892c..21fce315 100644 --- a/src/c8/lib/C8Dto.ts +++ b/src/c8/lib/C8Dto.ts @@ -126,7 +126,7 @@ export class CreateProcessInstanceResponse> { * in the DeployResourceResponse message) */ @Int64String - readonly processKey!: string + readonly processDefinitionKey!: string /** * The BPMN process ID of the process definition */ From f7ab6eb63f84ecc9ed616db6f52ddfe47e20613c Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Wed, 25 Sep 2024 01:14:48 +1200 Subject: [PATCH 23/34] refactor(camunda8): change name of C8RestClient -> CamundaRestClient --- package-lock.json | 11 + package.json | 1 + .../c8/rest/activateJobs.rest.spec.ts | 4 +- .../c8/rest/broadcastSigna.rest.spec.ts | 4 +- .../c8/rest/createProcess.rest.spec.ts | 4 +- .../c8/rest/deleteResource.rest.spec.ts | 4 +- .../c8/rest/migrateProcess.rest.spec.ts | 12 +- src/__tests__/c8/rest/pinClock.rest.spec.ts | 4 +- .../c8/rest/publishMessage.rest.spec.ts | 4 +- .../operate/operate-integration.spec.ts | 4 +- src/c8/index.ts | 19 +- .../{C8JobWorker.ts => CamundaJobWorker.ts} | 58 +++- .../{C8RestClient.ts => CamundaRestClient.ts} | 295 +++++++++++++----- src/index.ts | 31 +- src/lib/CreateDtoInstance.ts | 16 + src/lib/GotErrors.ts | 2 +- src/lib/LosslessJsonParser.ts | 10 +- src/lib/index.ts | 1 + src/zeebe/lib/ZBWorkerBase.ts | 6 +- src/zeebe/lib/interfaces-1.0.ts | 53 ++-- src/zeebe/zb/ZeebeGrpcClient.ts | 12 +- 21 files changed, 379 insertions(+), 176 deletions(-) rename src/c8/lib/{C8JobWorker.ts => CamundaJobWorker.ts} (77%) rename src/c8/lib/{C8RestClient.ts => CamundaRestClient.ts} (75%) create mode 100644 src/lib/CreateDtoInstance.ts diff --git a/package-lock.json b/package-lock.json index 45b95862..bfdae47d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,6 +69,7 @@ "ts-protoc-gen": "^0.15.0", "tsconfig-paths": "^4.2.0", "tsd": "^0.31.0", + "typed-emitter": "^2.1.0", "typedoc": "^0.25.9", "typedoc-plugin-include-example": "^1.2.0", "typedoc-plugin-missing-exports": "^2.2.0", @@ -18123,6 +18124,16 @@ "version": "1.0.13", "license": "ISC" }, + "node_modules/typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-2.1.0.tgz", + "integrity": "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "rxjs": "*" + } + }, "node_modules/typedarray": { "version": "0.0.6", "dev": true, diff --git a/package.json b/package.json index dc1282ab..f5f3ee41 100644 --- a/package.json +++ b/package.json @@ -131,6 +131,7 @@ "ts-protoc-gen": "^0.15.0", "tsconfig-paths": "^4.2.0", "tsd": "^0.31.0", + "typed-emitter": "^2.1.0", "typedoc": "^0.25.9", "typedoc-plugin-include-example": "^1.2.0", "typedoc-plugin-missing-exports": "^2.2.0", diff --git a/src/__tests__/c8/rest/activateJobs.rest.spec.ts b/src/__tests__/c8/rest/activateJobs.rest.spec.ts index c5918829..0a4133be 100644 --- a/src/__tests__/c8/rest/activateJobs.rest.spec.ts +++ b/src/__tests__/c8/rest/activateJobs.rest.spec.ts @@ -1,9 +1,9 @@ import path from 'node:path' -import { C8RestClient } from '../../../c8/lib/C8RestClient' +import { CamundaRestClient } from '../../../c8/lib/CamundaRestClient' let bpmnProcessId: string -const restClient = new C8RestClient() +const restClient = new CamundaRestClient() beforeAll(async () => { const res = await restClient.deployResourcesFromFiles([ diff --git a/src/__tests__/c8/rest/broadcastSigna.rest.spec.ts b/src/__tests__/c8/rest/broadcastSigna.rest.spec.ts index 353448e2..bddcc2c6 100644 --- a/src/__tests__/c8/rest/broadcastSigna.rest.spec.ts +++ b/src/__tests__/c8/rest/broadcastSigna.rest.spec.ts @@ -1,10 +1,10 @@ -import { C8RestClient } from '../../../c8/lib/C8RestClient' +import { CamundaRestClient } from '../../../c8/lib/CamundaRestClient' import { LosslessDto } from '../../../lib' import { cancelProcesses } from '../../../zeebe/lib/cancelProcesses' jest.setTimeout(60000) -const c8 = new C8RestClient() +const c8 = new CamundaRestClient() let pid: string beforeAll(async () => { diff --git a/src/__tests__/c8/rest/createProcess.rest.spec.ts b/src/__tests__/c8/rest/createProcess.rest.spec.ts index e0e2c62d..c76c5b23 100644 --- a/src/__tests__/c8/rest/createProcess.rest.spec.ts +++ b/src/__tests__/c8/rest/createProcess.rest.spec.ts @@ -1,13 +1,13 @@ import path from 'node:path' -import { C8RestClient } from '../../../c8/lib/C8RestClient' +import { CamundaRestClient } from '../../../c8/lib/CamundaRestClient' import { LosslessDto } from '../../../lib' jest.setTimeout(17000) let bpmnProcessId: string let processDefinitionKey: string -const restClient = new C8RestClient() +const restClient = new CamundaRestClient() beforeAll(async () => { const res = await restClient.deployResourcesFromFiles([ diff --git a/src/__tests__/c8/rest/deleteResource.rest.spec.ts b/src/__tests__/c8/rest/deleteResource.rest.spec.ts index 6dd6b298..175f03a9 100644 --- a/src/__tests__/c8/rest/deleteResource.rest.spec.ts +++ b/src/__tests__/c8/rest/deleteResource.rest.spec.ts @@ -1,6 +1,6 @@ -import { C8RestClient } from '../../../c8/lib/C8RestClient' +import { CamundaRestClient } from '../../../c8/lib/CamundaRestClient' -const c8 = new C8RestClient() +const c8 = new CamundaRestClient() test('It can delete a resource', async () => { const res = await c8.deployResourcesFromFiles([ diff --git a/src/__tests__/c8/rest/migrateProcess.rest.spec.ts b/src/__tests__/c8/rest/migrateProcess.rest.spec.ts index 6be1d4c8..9afce14b 100644 --- a/src/__tests__/c8/rest/migrateProcess.rest.spec.ts +++ b/src/__tests__/c8/rest/migrateProcess.rest.spec.ts @@ -1,11 +1,11 @@ import path from 'path' -import { C8JobWorker } from 'c8/lib/C8JobWorker' +import { CamundaJobWorker } from 'c8/lib/C8JobWorker' -import { C8RestClient } from '../../../c8/lib/C8RestClient' +import { CamundaRestClient } from '../../../c8/lib/CamundaRestClient' import { LosslessDto } from '../../../lib' -const c8 = new C8RestClient() +const c8 = new CamundaRestClient() class CustomHeaders extends LosslessDto { ProcessVersion!: number @@ -32,7 +32,7 @@ test('RestClient can migrate a process instance', async () => { let instanceKey = '' let processVersion = 0 - await new Promise>((res) => { + await new Promise>((res) => { const w = c8.createJobWorker({ type: 'migrant-rest-worker-task-1', maxJobsToActivate: 10, @@ -74,7 +74,7 @@ test('RestClient can migrate a process instance', async () => { // Complete the job in the process instance - await new Promise>((res) => { + await new Promise>((res) => { const w = c8.createJobWorker({ type: 'migration-rest-checkpoint', worker: 'Migrant Checkpoint worker', @@ -90,7 +90,7 @@ test('RestClient can migrate a process instance', async () => { }) }).then((w) => w.stop()) - await new Promise>((res) => { + await new Promise>((res) => { const w = c8.createJobWorker({ type: 'migrant-rest-worker-task-2', worker: 'Migrant Worker 2', diff --git a/src/__tests__/c8/rest/pinClock.rest.spec.ts b/src/__tests__/c8/rest/pinClock.rest.spec.ts index e796b77e..64535ab3 100644 --- a/src/__tests__/c8/rest/pinClock.rest.spec.ts +++ b/src/__tests__/c8/rest/pinClock.rest.spec.ts @@ -1,8 +1,8 @@ -import { C8RestClient } from '../../../c8/lib/C8RestClient' +import { CamundaRestClient } from '../../../c8/lib/CamundaRestClient' test('We can pin the clock, and reset it', async () => { const now = Date.now() - const c8 = new C8RestClient() + const c8 = new CamundaRestClient() await c8.pinInternalClock(now) // Pin the clock to the present time await c8.pinInternalClock(now + 1000) // Move the clock forward 1 second await c8.resetClock() // Reset the clock diff --git a/src/__tests__/c8/rest/publishMessage.rest.spec.ts b/src/__tests__/c8/rest/publishMessage.rest.spec.ts index b56b98be..59f5f57d 100644 --- a/src/__tests__/c8/rest/publishMessage.rest.spec.ts +++ b/src/__tests__/c8/rest/publishMessage.rest.spec.ts @@ -1,9 +1,9 @@ import { v4 } from 'uuid' -import { C8RestClient } from '../../../c8/lib/C8RestClient' +import { CamundaRestClient } from '../../../c8/lib/CamundaRestClient' import { LosslessDto } from '../../../lib' -const c8 = new C8RestClient() +const c8 = new CamundaRestClient() beforeAll(async () => { await c8.deployResourcesFromFiles([ diff --git a/src/__tests__/operate/operate-integration.spec.ts b/src/__tests__/operate/operate-integration.spec.ts index 35487422..dc1b8772 100644 --- a/src/__tests__/operate/operate-integration.spec.ts +++ b/src/__tests__/operate/operate-integration.spec.ts @@ -2,7 +2,7 @@ import { LosslessNumber } from 'lossless-json' import { HTTPError, - RESTError, + RestError, restoreZeebeLogging, suppressZeebeLogging, } from '../../lib' @@ -95,7 +95,7 @@ test('test error type', async () => { */ const res = await c .getProcessInstance(`${p.processInstanceKey}1`) - .catch((e: RESTError) => { + .catch((e: RestError) => { // console.log(e.code) // `ERR_NON_2XX_3XX_RESPONSE` diff --git a/src/c8/index.ts b/src/c8/index.ts index 44517ebc..d693f1ad 100644 --- a/src/c8/index.ts +++ b/src/c8/index.ts @@ -15,7 +15,7 @@ import { TasklistApiClient } from '../tasklist' import { ZeebeGrpcClient, ZeebeRestClient } from '../zeebe' import { getLogger } from './lib/C8Logger' -import { C8RestClient } from './lib/C8RestClient' +import { CamundaRestClient } from './lib/CamundaRestClient' /** * A single point of configuration for all Camunda Platform 8 clients. @@ -33,7 +33,7 @@ import { C8RestClient } from './lib/C8RestClient' * const tasklist = c8.getTasklistApiClient() * const modeler = c8.getModelerApiClient() * const admin = c8.getAdminApiClient() - * const c8Rest = c8.getC8RestClient() + * const c8Rest = c8.getCamundaRestClient() * ``` */ export class Camunda8 { @@ -46,9 +46,12 @@ export class Camunda8 { private zeebeRestClient?: ZeebeRestClient private configuration: CamundaPlatform8Configuration private oAuthProvider: IOAuthProvider - private c8RestClient?: C8RestClient + private camundaRestClient?: CamundaRestClient public log: winston.Logger + /** + * All constructor parameters for configuration are optional. If no configuration is provided, the SDK will use environment variables to configure itself. + */ constructor(config: Camunda8ClientConfiguration = {}) { this.configuration = CamundaEnvironmentConfigurator.mergeConfigWithEnvironment(config) @@ -117,7 +120,7 @@ export class Camunda8 { } /** - * @deprecated from 8.6. Please use getC8RestClient() instead. + * @deprecated from 8.6.0. Please use getCamundaRestClient() instead. */ public getZeebeRestClient(): ZeebeRestClient { if (!this.zeebeRestClient) { @@ -129,13 +132,13 @@ export class Camunda8 { return this.zeebeRestClient } - public getC8RestClient(): C8RestClient { - if (!this.c8RestClient) { - this.c8RestClient = new C8RestClient({ + public getCamundaRestClient(): CamundaRestClient { + if (!this.camundaRestClient) { + this.camundaRestClient = new CamundaRestClient({ config: this.configuration, oAuthProvider: this.oAuthProvider, }) } - return this.c8RestClient + return this.camundaRestClient } } diff --git a/src/c8/lib/C8JobWorker.ts b/src/c8/lib/CamundaJobWorker.ts similarity index 77% rename from src/c8/lib/C8JobWorker.ts rename to src/c8/lib/CamundaJobWorker.ts index 813646f3..61af2985 100644 --- a/src/c8/lib/C8JobWorker.ts +++ b/src/c8/lib/CamundaJobWorker.ts @@ -1,3 +1,6 @@ +import { EventEmitter } from 'events' + +import TypedEmitter from 'typed-emitter' import winston from 'winston' import { LosslessDto } from '../../lib' @@ -11,9 +14,24 @@ import { import { Ctor } from './C8Dto' import { getLogger } from './C8Logger' -import { C8RestClient } from './C8RestClient' +import { CamundaRestClient } from './CamundaRestClient' + +type CamundaJobWorkerEvents = { + pollError: (error: Error) => void + start: () => void + stop: () => void + poll: ({ + currentlyActiveJobCount, + maxJobsToActivate, + worker, + }: { + currentlyActiveJobCount: number + maxJobsToActivate: number + worker: string + }) => void +} -export interface C8JobWorkerConfig< +export interface CamundaJobWorkerConfig< VariablesDto extends LosslessDto, CustomHeadersDto extends LosslessDto, > extends ActivateJobsRequest { @@ -27,46 +45,56 @@ export interface C8JobWorkerConfig< log: winston.Logger ) => MustReturnJobActionAcknowledgement logger?: winston.Logger + /** Default: true. Start the worker polling immediately. If set to `false`, call the worker's `start()` method to start polling for work. */ + autoStart?: boolean } - -export class C8JobWorker< +// Make this class extend event emitter and have a typed event 'pollError' +export class CamundaJobWorker< VariablesDto extends LosslessDto, CustomHeadersDto extends LosslessDto, -> { +> extends (EventEmitter as new () => TypedEmitter) { public currentlyActiveJobCount = 0 public capacity: number private loopHandle?: NodeJS.Timeout private pollInterval: number - private log: winston.Logger + public log: winston.Logger logMeta: () => { worker: string type: string - pollInterval: number + pollIntervalMs: number capacity: number currentload: number } constructor( - private readonly config: C8JobWorkerConfig, - private readonly restClient: C8RestClient + private readonly config: CamundaJobWorkerConfig< + VariablesDto, + CustomHeadersDto + >, + private readonly restClient: CamundaRestClient ) { + super() this.pollInterval = config.pollIntervalMs ?? 30000 this.capacity = this.config.maxJobsToActivate this.log = getLogger({ logger: config.logger }) this.logMeta = () => ({ worker: this.config.worker, type: this.config.type, - pollInterval: this.pollInterval, + pollIntervalMs: this.pollInterval, capacity: this.config.maxJobsToActivate, currentload: this.currentlyActiveJobCount, }) this.log.debug(`Created REST Job Worker`, this.logMeta()) + if (config.autoStart) { + this.start() + } } start() { this.log.debug(`Starting poll loop`, this.logMeta()) + this.emit('start') this.poll() - this.loopHandle = setInterval(() => this.poll, this.pollInterval) + this.loopHandle = setInterval(() => this.poll(), this.pollInterval) } /** Stops the Job Worker polling for more jobs. If await this call, and it will return as soon as all currently active jobs are completed. @@ -79,6 +107,7 @@ export class C8JobWorker< return new Promise((resolve, reject) => { if (this.currentlyActiveJobCount === 0) { this.log.debug(`All jobs drained. Worker stopped.`, this.logMeta()) + this.emit('stop') return resolve(null) } /** This is an error timeout - if we don't complete all active jobs before the specified deadline, we reject the Promise */ @@ -96,6 +125,7 @@ export class C8JobWorker< this.log.debug(`All jobs drained. Worker stopped.`, this.logMeta()) clearInterval(wait) clearTimeout(timeout) + this.emit('stop') return resolve(null) } this.log.debug( @@ -107,6 +137,11 @@ export class C8JobWorker< } private poll() { + this.emit('poll', { + currentlyActiveJobCount: this.currentlyActiveJobCount, + maxJobsToActivate: this.config.maxJobsToActivate, + worker: this.config.worker, + }) if (this.currentlyActiveJobCount >= this.config.maxJobsToActivate) { this.log.debug(`At capacity - not requesting more jobs`, this.logMeta()) return @@ -128,6 +163,7 @@ export class C8JobWorker< // The job handlers for the activated jobs will run in parallel jobs.forEach((job) => this.handleJob(job)) }) + .catch((e) => this.emit('pollError', e)) } private async handleJob( diff --git a/src/c8/lib/C8RestClient.ts b/src/c8/lib/CamundaRestClient.ts similarity index 75% rename from src/c8/lib/C8RestClient.ts rename to src/c8/lib/CamundaRestClient.ts index 6788ed35..6ab5de04 100644 --- a/src/c8/lib/C8RestClient.ts +++ b/src/c8/lib/CamundaRestClient.ts @@ -57,24 +57,38 @@ import { TaskChangeSet, UpdateElementVariableRequest, } from './C8Dto' -import { C8JobWorker, C8JobWorkerConfig } from './C8JobWorker' import { getLogger } from './C8Logger' +import { CamundaJobWorker, CamundaJobWorkerConfig } from './CamundaJobWorker' import { createSpecializedRestApiJobClass } from './RestApiJobClassFactory' import { createSpecializedCreateProcessInstanceResponseClass } from './RestApiProcessInstanceClassFactory' -const trace = debug('camunda:zeebe') +const trace = debug('camunda:zeebe-rest') const CAMUNDA_REST_API_VERSION = 'v2' class DefaultLosslessDto extends LosslessDto {} - -export class C8RestClient { +/** + * The client for the unified Camunda 8 REST API. + * + * Logging: to enable debug tracing during development, you can set `DEBUG=camunda:zeebe-rest`. + * + * For production, you can pass in an instance of [winston.Logger](https://github.com/winstonjs/winston) 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 + * + */ +export class CamundaRestClient { private userAgentString: string private oAuthProvider: IOAuthProvider private rest: Promise private tenantId?: string - private log: winston.Logger + public log: winston.Logger + /** + * All constructor parameters for configuration are optional. If no configuration is provided, the SDK will use environment variables to configure itself. + */ constructor(options?: { config?: Camunda8ClientConfiguration oAuthProvider?: IOAuthProvider @@ -114,6 +128,17 @@ export class C8RestClient { ), ], beforeError: [gotBeforeErrorHook], + 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.silly(body) + }, + ], }, }) ) @@ -128,12 +153,24 @@ export class C8RestClient { 'user-agent': this.userAgentString, accept: '*/*', } - trace('headers', headers) + const safeHeaders = { + ...headers, + authorization: + headers.authorization.substring(0, 15) + + (headers.authorization.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 */ public async modifyAuthorization(req: PatchAuthorizationRequest) { const headers = await this.getHeaders() @@ -149,7 +186,11 @@ export class C8RestClient { } /** - * Broadcasts a signal. + * Broadcast a signal. + * + * Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/broadcast-signal/ + * + * @since 8.6.0 */ public async broadcastSignal(req: BroadcastSignalReq) { const headers = await this.getHeaders() @@ -173,8 +214,13 @@ export class C8RestClient { ) } - /* 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/ */ + /** + * 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 + */ public async completeUserTask({ userTaskKey, variables = {}, @@ -198,35 +244,51 @@ export class C8RestClient { ) } - /* Assigns a user task with the given key to the given assignee. */ + /** + * 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 + */ public async assignTask({ userTaskKey, assignee, allowOverride = true, action = 'assign', }: { + /** The key of the user task to assign. */ userTaskKey: string + /** The assignee for the user task. The assignee must not be empty or null. */ assignee: string + /** By default, the task is reassigned if it was already assigned. Set this to false to return an error in such cases. The task must then first be unassigned to be assigned again. Use this when you have users picking from group task queues to prevent race conditions. */ allowOverride?: boolean + /** A custom action value that will be accessible from user task events resulting from this endpoint invocation. If not provided, it will default to "assign". */ action: string }) { const headers = await this.getHeaders() - + const req = { + allowOverride, + action, + assignee, + } return this.rest.then((rest) => rest .post(`user-tasks/${userTaskKey}/assignment`, { - body: losslessStringify({ - allowOverride, - action, - assignee, - }), + body: losslessStringify(req), headers, }) .json() ) } - /** Update a user task with the given key. */ + /** + * 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 + */ public async updateTask({ userTaskKey, changeset, @@ -235,7 +297,6 @@ export class C8RestClient { changeset: TaskChangeSet }) { const headers = await this.getHeaders() - return this.rest.then((rest) => rest .patch(`user-tasks/${userTaskKey}/update`, { @@ -245,21 +306,29 @@ export class C8RestClient { .json() ) } - /* Removes the assignee of a task with the given key. */ + /** + * 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 + */ public async unassignTask({ userTaskKey }: { userTaskKey: string }) { const headers = await this.getHeaders() - return this.rest.then((rest) => rest.delete(`user-tasks/${userTaskKey}/assignee`, { headers }).json() ) } /** - * Create a user + * Create a user. + * + * Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/create-user/ + * + * @since 8.6.0 */ public async createUser(newUserInfo: NewUserInfo) { const headers = await this.getHeaders() - return this.rest.then((rest) => rest .post(`users`, { @@ -272,14 +341,19 @@ export class C8RestClient { /** * Search for user tasks based on given criteria. + * + * Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/query-user-tasks-alpha/ * @experimental */ - public async queryTasks() {} + // 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. - **/ + * 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 + */ public async correlateMessage( message: Pick< PublishMessageRequest, @@ -287,8 +361,8 @@ export class C8RestClient { > ) { const headers = await this.getHeaders() - const body = losslessStringify(this.addDefaultTenantId(message)) - + const req = this.addDefaultTenantId(message) + const body = losslessStringify(req) return this.rest.then((rest) => rest .post(`messages/correlation`, { @@ -301,14 +375,16 @@ export class C8RestClient { } /** - * Publishes a single message. Messages are published to specific partitions computed from their correlation keys. - * The endpoint does not wait for a correlation result. Use `correlateMessage` for such use cases. + * 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 */ public async publishMessage(publishMessageRequest: PublishMessageRequest) { const headers = await this.getHeaders() - const body = losslessStringify( - this.addDefaultTenantId(publishMessageRequest) - ) + const req = this.addDefaultTenantId(publishMessageRequest) + const body = losslessStringify(req) return this.rest.then((rest) => rest .post(`messages/publication`, { @@ -321,7 +397,11 @@ export class C8RestClient { } /** - * Obtains the status of the current Camunda license + * 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 */ public async getLicenseStatus(): Promise<{ vaildLicense: boolean @@ -333,21 +413,25 @@ export class C8RestClient { /** * 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. + * + * @since 8.6.0 */ public createJobWorker< Variables extends LosslessDto, CustomHeaders extends LosslessDto, - >(config: C8JobWorkerConfig) { - const worker = new C8JobWorker(config, this) - worker.start() + >(config: CamundaJobWorkerConfig) { + const worker = new CamundaJobWorker(config, this) + // worker.start() return worker } /** * Iterate through all known partitions and activate jobs up to the requested maximum. * - * The parameter Variables is a Dto to decode the job payload. The CustomHeaders parameter is a Dto to decode the custom headers. + * 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 other applications that natively support the int64 type. + * and safe interoperability with applications that use the `int64` type in variables. + * + * @since 8.6.0 */ public async activateJobs< VariablesDto extends LosslessDto, @@ -366,10 +450,17 @@ export class C8RestClient { const { inputVariableDto = LosslessDto, customHeadersDto = LosslessDto, + tenantIds = this.tenantId ? [this.tenantId] : undefined, ...req } = request - const body = losslessStringify(this.addDefaultTenantIds(req)) + /** + * The ActivateJobs endpoint can take multiple tenantIds, and activate jobs for multiple tenants at once. + */ + const body = losslessStringify({ + ...req, + tenantIds, + }) const jobDto = createSpecializedRestApiJobClass( inputVariableDto, @@ -390,13 +481,14 @@ export class C8RestClient { /** * 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. - * @throws + * * Documentation: https://docs.camunda.io/docs/next/apis-tools/camunda-api-rest/specifications/fail-job/ + * + * @since 8.6.0 */ public async failJob(failJobRequest: FailJobRequest) { const { jobKey } = failJobRequest const headers = await this.getHeaders() - return this.rest.then((rest) => rest .post(`jobs/${jobKey}/failure`, { @@ -408,16 +500,17 @@ export class C8RestClient { } /** - * Reports a business error (i.e. non-technical) that occurs while processing a job. - * @throws + * 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 */ 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`, { @@ -431,17 +524,19 @@ export class C8RestClient { /** * 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/ - * @throws + * + * @since 8.6.0 */ public async completeJob(completeJobRequest: 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: losslessStringify({ variables: completeJobRequest.variables }), + body: losslessStringify(req), headers, }) .then(() => JOB_ACTION_ACKNOWLEDGEMENT) @@ -450,13 +545,16 @@ export class C8RestClient { /** * 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 */ 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), @@ -467,10 +565,13 @@ export class C8RestClient { /** * 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 */ public async resolveIncident(incidentKey: string) { const headers = await this.getHeaders() - return this.rest.then((rest) => rest.post(`incidents/${incidentKey}/resolution`, { headers, @@ -480,6 +581,10 @@ export class C8RestClient { /** * Create and start a process instance. This method does not await the outcome of the process. For that, use `createProcessInstanceWithResult`. + * + * Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/create-process-instance/ + * + * @since 8.6.0 */ public async createProcessInstance( request: CreateProcessInstanceReq @@ -517,6 +622,10 @@ export class C8RestClient { /** * Create and start a process instance. This method awaits the outcome of the process. + * + * Documentation: https://docs.camunda.io/docs/apis-tools/camunda-api-rest/specifications/create-process-instance/ + * + * @since 8.6.0 */ public async createProcessInstanceWithResult( request: CreateProcessInstanceReq & { @@ -572,7 +681,6 @@ export class C8RestClient { operationReference?: number }) { const headers = await this.getHeaders() - return this.rest.then((rest) => rest.post(`process-instances/${processInstanceKey}/cancellation`, { body: JSON.stringify({ operationReference }), @@ -585,6 +693,10 @@ export class C8RestClient { * 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 */ public async migrateProcessInstance(req: MigrationRequest) { const headers = await this.getHeaders() @@ -604,6 +716,10 @@ export class C8RestClient { * 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 */ public async deployResources( resources: { content: string; name: string }[], @@ -642,15 +758,14 @@ export class C8RestClient { * 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 DeployResourceResponse({ - key: res.key.toString(), - tenantId: res.tenantId, - deployments: [], - processes: [], - decisions: [], - decisionRequirements: [], - forms: [], - }) + const deploymentResponse = new DeployResourceResponse() + deploymentResponse.key = res.key.toString() + deploymentResponse.tenantId = res.tenantId + deploymentResponse.deployments = [] + deploymentResponse.processes = [] + deploymentResponse.decisions = [] + deploymentResponse.decisionRequirements = [] + deploymentResponse.forms = [] /** * Type-guard assertions to correctly type the deployments. The API returns an array with mixed types. @@ -719,6 +834,8 @@ export class C8RestClient { /** * Deploy resources to Camunda 8 from files * @param files an array of file paths + * + * @since 8.6.0 */ public async deployResourcesFromFiles(files: string[]) { const resources: { content: string; name: string }[] = [] @@ -735,6 +852,10 @@ export class C8RestClient { /** * 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 */ public async deleteResource(req: { resourceKey: string @@ -754,6 +875,10 @@ export class C8RestClient { * 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 */ public async pinInternalClock(epochMs: number) { const headers = await this.getHeaders() @@ -769,12 +894,37 @@ export class C8RestClient { /** * 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 */ public async resetClock() { const headers = await this.getHeaders() return this.rest.then((rest) => rest.post(`clock/reset`, { headers })) } + /** + * 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 + */ + public async updateElementInstanceVariables( + req: UpdateElementVariableRequest + ) { + const headers = await this.getHeaders() + const { elementInstanceKey, ...request } = req + return this.rest.then((rest) => + rest.post(`element-instances/${elementInstanceKey}/variables`, { + headers, + body: stringify(request), + }) + ) + } + private addJobMethods = ( job: Job ): Job & @@ -802,23 +952,6 @@ export class C8RestClient { } } - /** - * 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. - */ - public async updateElementInstanceVariables( - req: UpdateElementVariableRequest - ) { - const headers = await this.getHeaders() - const { elementInstanceKey, ...request } = req - return this.rest.then((rest) => - rest.post(`element-instances/${elementInstanceKey}/variables`, { - headers, - body: stringify(request), - }) - ) - } - /** * Helper method to add the default tenantIds if we are not passed explicit tenantIds */ @@ -826,12 +959,4 @@ export class C8RestClient { const tenantId = request.tenantId ?? this.tenantId return { ...request, tenantId } } - - /** - * Helper method to add the default tenantIds if we are not passed explicit tenantIds - */ - private addDefaultTenantIds(request: T) { - const tenantIds = request.tenantIds ?? [this.tenantId] - return { ...request, tenantIds } - } } diff --git a/src/index.ts b/src/index.ts index d9ee8014..6255b6de 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,13 @@ import * as Admin from './admin' import { Camunda8 } from './c8' -import { BigIntValue, ChildDto, Int64String, LosslessDto } from './lib' +import { CamundaRestClient } from './c8/lib/CamundaRestClient' +import { + BigIntValue, + ChildDto, + Int64String, + LosslessDto, + createDtoInstance, +} from './lib' import * as Modeler from './modeler' import * as Auth from './oauth' import * as Operate from './operate' @@ -8,8 +15,24 @@ import * as Optimize from './optimize' import * as Tasklist from './tasklist' import * as Zeebe from './zeebe' -export { /*HTTPError,*/ RESTError } from './lib' +export { HTTPError } from './lib' -export const Dto = { ChildDto, BigIntValue, Int64String, LosslessDto } +export const Dto = { + ChildDto, + BigIntValue, + Int64String, + LosslessDto, + createDtoInstance, +} -export { Admin, Auth, Camunda8, Modeler, Operate, Optimize, Tasklist, Zeebe } +export { + Admin, + Auth, + Camunda8, + CamundaRestClient, + Modeler, + Operate, + Optimize, + Tasklist, + Zeebe, +} diff --git a/src/lib/CreateDtoInstance.ts b/src/lib/CreateDtoInstance.ts new file mode 100644 index 00000000..6ad16f62 --- /dev/null +++ b/src/lib/CreateDtoInstance.ts @@ -0,0 +1,16 @@ +/** + * Create an instance of a DTO class with the provided data. + * + * This provides a type-safe method to create a DTO instance from a plain object. + * + * @param dtoClass + * @param dtoData + * @returns + */ +export function createDtoInstance(dtoClass: { new (): T }, dtoData: T) { + const newDto = new dtoClass() + for (const key in dtoData) { + newDto[key] = dtoData[key] + } + return newDto +} diff --git a/src/lib/GotErrors.ts b/src/lib/GotErrors.ts index 94d7dd47..bae73137 100644 --- a/src/lib/GotErrors.ts +++ b/src/lib/GotErrors.ts @@ -20,7 +20,7 @@ export class HTTPError extends Got.HTTPError { } } -export type RESTError = +export type RestError = | HTTPError | Got.RequestError | Got.ReadError diff --git a/src/lib/LosslessJsonParser.ts b/src/lib/LosslessJsonParser.ts index 39377bf5..8817830b 100644 --- a/src/lib/LosslessJsonParser.ts +++ b/src/lib/LosslessJsonParser.ts @@ -119,15 +119,7 @@ export function ChildDto(childClass: any) { * } * ``` */ -export class LosslessDto { - constructor(obj?: any) { - if (obj) { - for (const [key, value] of Object.entries(obj)) { - this[key] = value - } - } - } -} +export class LosslessDto {} /** * losslessParse uses lossless-json parse to deserialize JSON. diff --git a/src/lib/index.ts b/src/lib/index.ts index af6d9a5b..58adb557 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,6 +1,7 @@ export * from './ClientConstructor' export * from './Configuration' export * from './ConstructOAuthProvider' +export * from './CreateDtoInstance' export * from './CreateUserAgentString' export * from './Delay' export * from './EnvironmentSetup' diff --git a/src/zeebe/lib/ZBWorkerBase.ts b/src/zeebe/lib/ZBWorkerBase.ts index 6c402ad6..efdbe112 100644 --- a/src/zeebe/lib/ZBWorkerBase.ts +++ b/src/zeebe/lib/ZBWorkerBase.ts @@ -140,14 +140,12 @@ export class ZBWorkerBase< this.inputVariableDto = inputVariableDto ?? (LosslessDto as { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - new (obj: any): WorkerInputVariables + new (): WorkerInputVariables }) this.customHeadersDto = customHeadersDto ?? (LosslessDto as { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - new (obj: any): CustomHeaderShape + new (): CustomHeaderShape }) this.taskHandler = taskHandler this.taskType = taskType diff --git a/src/zeebe/lib/interfaces-1.0.ts b/src/zeebe/lib/interfaces-1.0.ts index e921c615..b3070467 100644 --- a/src/zeebe/lib/interfaces-1.0.ts +++ b/src/zeebe/lib/interfaces-1.0.ts @@ -40,7 +40,7 @@ import { } from './interfaces-grpc-1.0' import { Loglevel, ZBCustomLogger } from './interfaces-published-contract' -// The JSON-stringified version of this is sent to the ZBCustomLogger +/** The JSON-stringified version of this is sent to the ZBCustomLogger */ export interface ZBLogMessage { timestamp: Date context: string @@ -52,11 +52,11 @@ export interface ZBLogMessage { export interface CreateProcessBaseRequest { /** - * the BPMN process ID of the process definition + * The BPMN process ID of the process definition */ bpmnProcessId: string /** - * the version of the process; if not specified it will use the latest version + * The version of the process; if not specified it will use the latest version */ version?: number /** @@ -64,9 +64,9 @@ export interface CreateProcessBaseRequest { * process instance. */ variables: V - /** The tenantId for a multi-tenant enabled cluster. */ + /** The `tenantId` for a multi-tenant enabled cluster. */ tenantId?: string - /** a reference key chosen by the user and will be part of all records resulted from this operation */ + /** A reference key chosen by the user and will be part of all records resulted from this operation */ operationReference?: number | LosslessNumber } @@ -83,12 +83,12 @@ export interface CreateProcessInstanceReq export interface CreateProcessInstanceWithResultReq extends CreateProcessBaseRequest { /** - * timeout in milliseconds. the request will be closed if the process is not completed before the requestTimeout. + * Timeout in milliseconds. the request will be closed if the process is not completed before the requestTimeout. * if requestTimeout = 0, uses the generic requestTimeout configured in the gateway. */ requestTimeout?: number /** - * list of names of variables to be included in `CreateProcessInstanceWithResultResponse.variables`. + * List of names of variables to be included in `CreateProcessInstanceWithResultResponse.variables`. * If empty, all visible variables in the root scope will be returned. */ fetchVariables?: string[] @@ -316,14 +316,14 @@ export interface Job< readonly worker: string /* The amount of retries left to this job (should always be positive) */ readonly retries: number - // epoch milliseconds + /** Epoch milliseconds */ readonly deadline: string /** * All visible variables in the task scope, computed at activation time. */ readonly variables: Readonly /** - * TenantId of the job in a multi-tenant cluster + * The `tenantId` of the job in a multi-tenant cluster */ readonly tenantId: string } @@ -431,69 +431,70 @@ export interface ZBWorkerConfig< // eslint-disable-next-line @typescript-eslint/no-explicit-any customHeadersDto?: { new (...args: any[]): Readonly } /** - * An optional array of tenantIds if you want this to be a multi-tenant worker. + * An optional array of `tenantId`s if you want this to be a multi-tenant worker. */ tenantIds?: string[] } export interface BroadcastSignalReq { - // The name of the signal + /** The name of the signal */ signalName: string - // the signal variables as a JSON document; to be valid, the root of the document must be an - // object, e.g. { "a": "foo" }. [ "foo" ] would not be valid. + /** + * The signal variables as a JSON document; to be valid, the root of the document must be an object, e.g. { "a": "foo" }. [ "foo" ] would not be valid. + */ variables?: JSONDoc - // Optional tenantId for a multi-tenant enabled cluster. This could also be supplied via environment variable. + /** Optional `tenantId` for a multi-tenant enabled cluster. This could also be supplied via environment variable. */ tenantId?: string } export interface BroadcastSignalRes { - // the unique ID of the signal that was broadcasted. + /** The unique ID of the signal that was broadcasted. */ key: string } export interface ResolveIncidentReq { readonly incidentKey: string - /** a reference key chosen by the user and will be part of all records resulted from this operation */ + /** A reference key chosen by the user and will be part of all records resulted from this operation */ operationReference?: number | LosslessNumber } export interface UpdateJobRetriesReq { readonly jobKey: string retries: number - /** a reference key chosen by the user and will be part of all records resulted from this operation */ + /** A reference key chosen by the user and will be part of all records resulted from this operation */ operationReference?: number | LosslessNumber } export interface UpdateJobTimeoutReq { readonly jobKey: string - /** the duration of the new timeout in ms, starting from the current moment */ + /** The duration of the new timeout in ms, starting from the current moment */ timeout: number - /** a reference key chosen by the user and will be part of all records resulted from this operation */ + /** A reference key chosen by the user and will be part of all records resulted from this operation */ operationReference?: number | LosslessNumber } export interface ModifyProcessInstanceReq { - /** the key of the process instance that should be modified */ + /** The key of the process instance that should be modified */ processInstanceKey: string /** - * instructions describing which elements should be activated in which scopes, + * Instructions describing which elements should be activated in which scopes, * and which variables should be created */ activateInstructions?: ActivateInstruction[] - /** instructions describing which elements should be terminated */ + /** Instructions describing which elements should be terminated */ terminateInstructions?: TerminateInstruction[] - /** a reference key chosen by the user and will be part of all records resulted from this operation */ + /** A reference key chosen by the user and will be part of all records resulted from this operation */ operationReference?: number | LosslessNumber } export interface MigrateProcessInstanceReq { - // key of the process instance to migrate + /** Key of the process instance to migrate */ processInstanceKey: string - // the migration plan that defines target process and element mappings + /** The migration plan that defines target process and element mappings */ migrationPlan: MigrationPlan - /** a reference key chosen by the user and will be part of all records resulted from this operation */ + /** A reference key chosen by the user and will be part of all records resulted from this operation */ operationReference?: number | LosslessNumber } diff --git a/src/zeebe/zb/ZeebeGrpcClient.ts b/src/zeebe/zb/ZeebeGrpcClient.ts index 7d57d832..bfeda8e9 100644 --- a/src/zeebe/zb/ZeebeGrpcClient.ts +++ b/src/zeebe/zb/ZeebeGrpcClient.ts @@ -269,14 +269,12 @@ export class ZeebeGrpcClient extends TypedEmitter< const inputVariableDtoToUse = inputVariableDto ?? (LosslessDto as { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - new (obj: any): Variables + new (): Variables }) const customHeadersDtoToUse = customHeadersDto ?? (LosslessDto as { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - new (obj: any): CustomHeaders + new (): CustomHeaders }) // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { @@ -1224,14 +1222,12 @@ export class ZeebeGrpcClient extends TypedEmitter< const inputVariableDto = req.inputVariableDto ? req.inputVariableDto : (LosslessDto as { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - new (obj: any): WorkerInputVariables + new (): WorkerInputVariables }) const customHeadersDto = req.customHeadersDto ? req.customHeadersDto : (LosslessDto as { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - new (obj: any): CustomHeaderShape + new (): CustomHeaderShape }) const fetchVariable = req.fetchVariables delete req.fetchVariables From d69729ad07f05649a59b9f89282dc01f87698b4f Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Wed, 25 Sep 2024 01:17:43 +1200 Subject: [PATCH 24/34] fix(lossless-parser): correctly parse number array fixes #258 --- .../lib/LosslessJsonParser.unit.spec.ts | 19 ++++++++++++++----- src/lib/CreateDtoInstance.ts | 4 ++++ src/lib/LosslessJsonParser.ts | 3 +++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/__tests__/lib/LosslessJsonParser.unit.spec.ts b/src/__tests__/lib/LosslessJsonParser.unit.spec.ts index 25f58b4f..1dc54d5b 100644 --- a/src/__tests__/lib/LosslessJsonParser.unit.spec.ts +++ b/src/__tests__/lib/LosslessJsonParser.unit.spec.ts @@ -1,6 +1,7 @@ import { BigIntValue, ChildDto, + createDtoInstance, Int64String, LosslessDto, losslessParse, @@ -387,14 +388,22 @@ test('LosslessJsonParser will throw if given stringified JSON with an unsafe int test('It rejects Date, Map, and Set types', () => { class Dto extends LosslessDto { - date!: Date - name!: string + date?: Date + name?: string + map?: Map + set?: Set } const date = new Date() - const dto = new Dto({ date, name: 'me' }) + const dto = createDtoInstance(Dto, { date, name: 'me' }) expect(() => losslessStringify(dto)).toThrow('Date') - const mapDto = new Dto({ map: new Map() }) + const mapDto = createDtoInstance(Dto, { map: new Map() }) expect(() => losslessStringify(mapDto)).toThrow('Map') - const setDto = new Dto({ set: new Set() }) + const setDto = createDtoInstance(Dto, { set: new Set() }) expect(() => losslessStringify(setDto)).toThrow('Set') }) + +test('It correctly handles a number array in a subkey', () => { + const json = `{"message":"Hello from automation","userId":null,"sendTo":[12022907,12022896,12022831]}` + const res = losslessParse(json) + expect(res.sendTo[0]).toBe(12022907) +}) diff --git a/src/lib/CreateDtoInstance.ts b/src/lib/CreateDtoInstance.ts index 6ad16f62..9350959f 100644 --- a/src/lib/CreateDtoInstance.ts +++ b/src/lib/CreateDtoInstance.ts @@ -3,6 +3,10 @@ * * This provides a type-safe method to create a DTO instance from a plain object. * + * Node 22's experimental strip types does not play well with the previous "via the constructor" method. + * + * See: https://gist.github.com/jwulf/6e7b093b5b7b3e12c7b76f55b9e4be84 + * * @param dtoClass * @param dtoData * @returns diff --git a/src/lib/LosslessJsonParser.ts b/src/lib/LosslessJsonParser.ts index 8817830b..12ced89c 100644 --- a/src/lib/LosslessJsonParser.ts +++ b/src/lib/LosslessJsonParser.ts @@ -262,6 +262,9 @@ function convertLosslessNumbersToNumberOrThrow(obj: any): T { if (!obj) { return obj } + if (obj instanceof LosslessNumber) { + return toSafeNumberOrThrow(obj.toString()) as T + } let currentKey = '' try { Object.keys(obj).forEach((key) => { From be308b0a48b4ec2e9174ee052c2fea87b22582df Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Wed, 25 Sep 2024 11:18:44 +1200 Subject: [PATCH 25/34] refactor(lossless-parser): improve array handling --- .../lib/LosslessJsonParser.unit.spec.ts | 53 ++++++- src/lib/LosslessJsonParser.ts | 146 ++++++++++++++++-- 2 files changed, 188 insertions(+), 11 deletions(-) diff --git a/src/__tests__/lib/LosslessJsonParser.unit.spec.ts b/src/__tests__/lib/LosslessJsonParser.unit.spec.ts index 1dc54d5b..934254ec 100644 --- a/src/__tests__/lib/LosslessJsonParser.unit.spec.ts +++ b/src/__tests__/lib/LosslessJsonParser.unit.spec.ts @@ -1,8 +1,10 @@ import { BigIntValue, + BigIntValueArray, ChildDto, createDtoInstance, Int64String, + Int64StringArray, LosslessDto, losslessParse, losslessStringify, @@ -350,7 +352,6 @@ test('LosslessJsonParser handles subkeys', () => { const jsonString = `{"jobs":[{"key":2251799813737371,"type":"console-log-complete","processInstanceKey":2251799813737366,"bpmnProcessId":"hello-world-complete","processDefinitionVersion":1,"processDefinitionKey":2251799813736299,"elementId":"ServiceTask_0g6tf5f","elementInstanceKey":2251799813737370,"customHeaders":{"message":"Hello World"},"worker":"test","retries":100,"deadline":1725501895792,"variables":{},"tenantId":""}]}` const parsed = losslessParse(jsonString, undefined, 'jobs') - console.log(parsed) expect(parsed[0].key).toBe(2251799813737371) }) @@ -407,3 +408,53 @@ test('It correctly handles a number array in a subkey', () => { const res = losslessParse(json) expect(res.sendTo[0]).toBe(12022907) }) + +test('It correctly handles a number array in a subkey with a DTO (Int64StringArray)', () => { + class Dto extends LosslessDto { + message!: string + userId!: number + @Int64StringArray + sendTo!: string[] + } + + const json = `{"message":"Hello from automation","userId":null,"sendTo":[12022907,12022896,12022831]}` + const res = losslessParse(json, Dto) + expect(res.sendTo[0]).toBe('12022907') +}) + +test('It correctly handles a number array in a subkey with a DTO (BigIntValueArray)', () => { + class Dto extends LosslessDto { + message!: string + userId!: number + @BigIntValueArray + sendTo!: string[] + } + + const json = `{"message":"Hello from automation","userId":null,"sendTo":[12022907,12022896,12022831]}` + const res = losslessParse(json, Dto) + expect(res.sendTo[0]).toBe(BigInt('12022907')) +}) + +test('It correctly throws when encountering a number rather than an array in a subkey with a DTO (Int64StringArray)', () => { + class Dto extends LosslessDto { + message!: string + userId!: number + @Int64StringArray + sendTo!: string[] + } + + const json = `{"message":"Hello from automation","userId":null,"sendTo":12022907}` + expect(() => losslessParse(json, Dto)).toThrow('expected Array') +}) + +test('It correctly throws when encountering a number rather than an array in a subkey with a DTO (BigIntValueArray)', () => { + class Dto extends LosslessDto { + message!: string + userId!: number + @BigIntValueArray + sendTo!: string[] + } + + const json = `{"message":"Hello from automation","userId":null,"sendTo":12022907}` + expect(() => losslessParse(json, Dto)).toThrow('expected Array') +}) diff --git a/src/lib/LosslessJsonParser.ts b/src/lib/LosslessJsonParser.ts index 12ced89c..4486f13b 100644 --- a/src/lib/LosslessJsonParser.ts +++ b/src/lib/LosslessJsonParser.ts @@ -33,6 +33,13 @@ import 'reflect-metadata' const debug = d('lossless-json-parser') +const MetadataKey = { + INT64_STRING: 'type:int64', + INT64_STRING_ARRAY: 'type:int64[]', + INT64_BIGINT: 'type:bigint', + INT64_BIGINT_ARRAY: 'type:bigint[]', + CHILD_DTO: 'child:class', +} /** * Decorate Dto string fields as `@Int64String` to specify that the JSON number property should be parsed as a string. * @example @@ -51,11 +58,34 @@ const debug = d('lossless-json-parser') * ``` */ export function Int64String(target: any, propertyKey: string | symbol): void { - Reflect.defineMetadata('type:int64', true, target, propertyKey) + Reflect.defineMetadata(MetadataKey.INT64_STRING, true, target, propertyKey) +} + +/** + * Decorate Dto string fields as `@Int64StringArray` to specify that the array of JSON numbers should be parsed as an array of strings. + * @example + * ```typescript + * class Dto extends LosslessDto { + * message!: string + * userId!: number + * @Int64StringArray + * sendTo!: string[] + * } + */ +export function Int64StringArray( + target: any, + propertyKey: string | symbol +): void { + Reflect.defineMetadata( + MetadataKey.INT64_STRING_ARRAY, + true, + target, + propertyKey + ) } /** - * Decorate Dto bigint fields as `@BigInt` to specify that the JSON number property should be parsed as a bigint. + * Decorate Dto bigint fields as `@BigIntValue` to specify that the JSON number property should be parsed as a bigint. * @example * ```typescript * class MyDto extends LosslessDto { @@ -72,9 +102,37 @@ export function Int64String(target: any, propertyKey: string | symbol): void { * ``` */ export function BigIntValue(target: any, propertKey: string | symbol): void { - Reflect.defineMetadata('type:bigint', true, target, propertKey) + Reflect.defineMetadata(MetadataKey.INT64_BIGINT, true, target, propertKey) } +/** + * Decorate Dto bigint fields as `@BigIntValueArray` to specify that the JSON number property should be parsed as a bigint. + * @example + * ```typescript + * class MyDto extends LosslessDto { + * @Int64String + * int64NumberField!: string + * @BigIntValueArray + * bigintField!: bigint[] + * @ChildDto(MyChildDto) + * childDtoField!: MyChildDto + * normalField!: string + * normalNumberField!: number + * maybePresentField?: string + * } + * ``` + */ +export function BigIntValueArray( + target: any, + propertKey: string | symbol +): void { + Reflect.defineMetadata( + MetadataKey.INT64_BIGINT_ARRAY, + true, + target, + propertKey + ) +} /** * Decorate a Dto object field as `@ChildDto` to specify that the JSON object property should be parsed as a child Dto. * @example @@ -98,7 +156,12 @@ export function BigIntValue(target: any, propertKey: string | symbol): void { */ export function ChildDto(childClass: any) { return function (target: any, propertyKey: string | symbol) { - Reflect.defineMetadata('child:class', childClass, target, propertyKey) + Reflect.defineMetadata( + MetadataKey.CHILD_DTO, + childClass, + target, + propertyKey + ) } } @@ -194,7 +257,11 @@ function parseWithAnnotations( const instance = new dto() for (const [key, value] of Object.entries(obj)) { - const childClass = Reflect.getMetadata('child:class', dto.prototype, key) + const childClass = Reflect.getMetadata( + MetadataKey.CHILD_DTO, + dto.prototype, + key + ) if (childClass) { if (Array.isArray(value)) { // If the value is an array, parse each element with the specified child class @@ -206,23 +273,82 @@ function parseWithAnnotations( instance[key] = losslessParse(stringify(value)!, childClass) } } else { - if (Reflect.hasMetadata('type:int64', dto.prototype, key)) { + if ( + Reflect.hasMetadata(MetadataKey.INT64_STRING_ARRAY, dto.prototype, key) + ) { + debug(`Parsing int64 array field "${key}" to string`) + if (Array.isArray(value)) { + instance[key] = value.map((item) => { + if (isLosslessNumber(item)) { + return item.toString() + } else { + debug('Unexpected type for value', value) + throw new Error( + `Unexpected type: Received JSON ${typeof item} value for Int64String Dto field "${key}", expected number` + ) + } + }) + } else { + const type = value instanceof LosslessNumber ? 'number' : typeof value + throw new Error( + `Unexpected type: Received JSON ${type} value for Int64StringArray Dto field "${key}", expected Array` + ) + } + } else if ( + Reflect.hasMetadata(MetadataKey.INT64_STRING, dto.prototype, key) + ) { debug(`Parsing int64 field "${key}" to string`) if (value) { if (isLosslessNumber(value)) { instance[key] = value.toString() } else { + if (Array.isArray(value)) { + throw new Error( + `Unexpected type: Received JSON array value for Int64String Dto field "${key}", expected number. If you are expecting an array, use the @Int64StringArray decorator.` + ) + } + const type = + value instanceof LosslessNumber ? 'number' : typeof value + throw new Error( - `Unexpected type: Received JSON ${typeof value} value for Int64String Dto field "${key}", expected number` + `Unexpected type: Received JSON ${type} value for Int64String Dto field "${key}", expected number` ) } } - } else if (Reflect.hasMetadata('type:bigint', dto.prototype, key)) { + } else if ( + Reflect.hasMetadata(MetadataKey.INT64_BIGINT_ARRAY, dto.prototype, key) + ) { + debug(`Parsing int64 array field "${key}" to BigInt`) + if (Array.isArray(value)) { + instance[key] = value.map((item) => { + if (isLosslessNumber(item)) { + return BigInt(item.toString()) + } else { + debug('Unexpected type for value', value) + throw new Error( + `Unexpected type: Received JSON ${typeof item} value for BigIntValue in Dto field "${key}[]", expected number` + ) + } + }) + } else { + const type = value instanceof LosslessNumber ? 'number' : typeof value + throw new Error( + `Unexpected type: Received JSON ${type} value for BigIntValueArray Dto field "${key}", expected Array` + ) + } + } else if ( + Reflect.hasMetadata(MetadataKey.INT64_BIGINT, dto.prototype, key) + ) { debug(`Parsing bigint field ${key}`) if (value) { if (isLosslessNumber(value)) { instance[key] = BigInt(value.toString()) } else { + if (Array.isArray(value)) { + throw new Error( + `Unexpected type: Received JSON array value for BigIntValue Dto field "${key}", expected number. If you are expecting an array, use the @BigIntValueArray decorator.` + ) + } throw new Error( `Unexpected type: Received JSON ${typeof value} value for BigIntValue Dto field "${key}", expected number` ) @@ -327,11 +453,11 @@ export function losslessStringify( if (typeof value === 'object' && value !== null) { // If the value is an object or array, recurse into it newObj[key] = losslessStringify(value, false) - } else if (Reflect.getMetadata('type:int64', obj, key)) { + } else if (Reflect.getMetadata(MetadataKey.INT64_STRING, obj, key)) { // If the property is decorated with @Int64String, convert the string to a LosslessNumber debug(`Stringifying int64 string field ${key}`) newObj[key] = new LosslessNumber(value) - } else if (Reflect.getMetadata('type:bigint', obj, key)) { + } else if (Reflect.getMetadata(MetadataKey.INT64_BIGINT, obj, key)) { // If the property is decorated with @BigIntValue, convert the bigint to a LosslessNumber debug(`Stringifying bigint field ${key}`) newObj[key] = new LosslessNumber(value.toString()) From cb95946ab989f86a5992900292db2227be0824db Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Wed, 25 Sep 2024 22:02:08 +1200 Subject: [PATCH 26/34] fix(camunda8): correctly parse autostart parameter of JobWorker --- src/c8/lib/CamundaJobWorker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/c8/lib/CamundaJobWorker.ts b/src/c8/lib/CamundaJobWorker.ts index 61af2985..dfa417a9 100644 --- a/src/c8/lib/CamundaJobWorker.ts +++ b/src/c8/lib/CamundaJobWorker.ts @@ -85,7 +85,7 @@ export class CamundaJobWorker< currentload: this.currentlyActiveJobCount, }) this.log.debug(`Created REST Job Worker`, this.logMeta()) - if (config.autoStart) { + if (config.autoStart ?? true) { this.start() } } From 3055734f521341aff850b6545c8919d38d9642a6 Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Wed, 25 Sep 2024 22:03:57 +1200 Subject: [PATCH 27/34] fix(camunda8): type variables in async process instance start as never --- src/c8/lib/CamundaRestClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/c8/lib/CamundaRestClient.ts b/src/c8/lib/CamundaRestClient.ts index 6ab5de04..a62f0557 100644 --- a/src/c8/lib/CamundaRestClient.ts +++ b/src/c8/lib/CamundaRestClient.ts @@ -588,7 +588,7 @@ export class CamundaRestClient { */ public async createProcessInstance( request: CreateProcessInstanceReq - ): Promise>> + ): Promise> async createProcessInstance< T extends JSONDoc | LosslessDto, From 1b7715e0e2778b058d7e0d8b67f29a27007c06af Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Thu, 26 Sep 2024 16:46:08 +1200 Subject: [PATCH 28/34] feat(repo): support passing middleware fixes #261 --- README.md | 24 +++++++++- docker/.env | 3 +- docker/docker-compose-multitenancy.yaml | 2 +- .../c8/rest/createProcess.rest.spec.ts | 10 ++-- .../c8/rest/migrateProcess.rest.spec.ts | 2 +- src/admin/lib/AdminApiClient.ts | 1 + src/c8/index.ts | 48 ++++++++++++------- src/c8/lib/CamundaRestClient.ts | 1 + src/lib/Configuration.ts | 5 +- src/modeler/lib/ModelerAPIClient.ts | 1 + src/operate/lib/OperateApiClient.ts | 1 + src/optimize/lib/OptimizeApiClient.ts | 1 + src/tasklist/lib/TasklistApiClient.ts | 1 + 13 files changed, 74 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 703e4ba7..cc768fb2 100644 --- a/README.md +++ b/README.md @@ -222,9 +222,31 @@ Here is an example of turning on debugging for the OAuth and Operate components: DEBUG=camunda:oauth,camunda:operate node app.js ``` +## Process Variable Typing + +Process variables - the `variables` of Zeebe messages, jobs, and process instance creation requests and responses - are stored in the broker as key:value pairs. They are transported as a JSON string. The SDK parses the JSON string into a JavaScript object. + +Various Zeebe methods accept DTO classes for variable input and output. These DTO classes are used to provide design-time type information on the `variables` object. They are also used to safely decode 64-bit integer values that cannot be accurately represented by the JavaScript `number` type. + +To create a DTO to represent the expected shape and type of the `variables` object, extend the `LosslessDto` class: + +```typescript +class myVariableDTO extends LosslessDto { + firstName!: string + lastName!: string + age!: number + optionalValue?: number + @Int64String + veryBigInteger?: string + constructor(data: Partial) { + super(data) + } +} +``` + ## Typing of Zeebe worker variables -The variable payload in a Zeebe worker task handler is available as an object `job.variables`. By default, this is of type `any`. +The variable payload in a Zeebe worker task handler is available as an object `job.variables`. By default, this is of type `any` for the gRPC API, and `unknown` for the REST API. The `ZBClient.createWorker()` method accepts an `inputVariableDto` to control the parsing of number values and provide design-time type information. Passing an `inputVariableDto` class to a Zeebe worker is optional. If a DTO class is passed to the Zeebe worker, it is used for two purposes: diff --git a/docker/.env b/docker/.env index c72a4440..66cf4d53 100644 --- a/docker/.env +++ b/docker/.env @@ -2,7 +2,8 @@ # CAMUNDA_CONNECTORS_VERSION=0.23.2 CAMUNDA_CONNECTORS_VERSION=8.5.0 CAMUNDA_OPTIMIZE_VERSION=8.5.0 -CAMUNDA_PLATFORM_VERSION=8.5.0 +CAMUNDA_PLATFORM_VERSION=8.6.0 +CAMUNDA_ZEEBE_VERSION=SNAPSHOT CAMUNDA_WEB_MODELER_VERSION=8.5.0 ELASTIC_VERSION=8.9.0 KEYCLOAK_SERVER_VERSION=22.0.3 diff --git a/docker/docker-compose-multitenancy.yaml b/docker/docker-compose-multitenancy.yaml index 366b1f95..2a84221b 100644 --- a/docker/docker-compose-multitenancy.yaml +++ b/docker/docker-compose-multitenancy.yaml @@ -11,7 +11,7 @@ services: zeebe: # https://docs.camunda.io/docs/self-managed/platform-deployment/docker/#zeebe - image: camunda/zeebe:${CAMUNDA_PLATFORM_VERSION} + image: camunda/zeebe:${CAMUNDA_ZEEBE_VERSION} container_name: zeebe ports: - "26500:26500" diff --git a/src/__tests__/c8/rest/createProcess.rest.spec.ts b/src/__tests__/c8/rest/createProcess.rest.spec.ts index c76c5b23..72c47e5d 100644 --- a/src/__tests__/c8/rest/createProcess.rest.spec.ts +++ b/src/__tests__/c8/rest/createProcess.rest.spec.ts @@ -1,7 +1,7 @@ import path from 'node:path' import { CamundaRestClient } from '../../../c8/lib/CamundaRestClient' -import { LosslessDto } from '../../../lib' +import { createDtoInstance, LosslessDto } from '../../../lib' jest.setTimeout(17000) @@ -52,7 +52,7 @@ test('Can create a process with a lossless Dto', (done) => { restClient .createProcessInstance({ processDefinitionKey, - variables: new myVariableDto({ someNumberField: 8 }), + variables: createDtoInstance(myVariableDto, { someNumberField: 8 }), }) .then((res) => { expect(res.processDefinitionKey).toEqual(processDefinitionKey) @@ -61,7 +61,7 @@ test('Can create a process with a lossless Dto', (done) => { }) test('Can create a process and get the result', (done) => { - const variables = new myVariableDto({ someNumberField: 8 }) + const variables = createDtoInstance(myVariableDto, { someNumberField: 8 }) restClient .createProcessInstanceWithResult({ processDefinitionKey, @@ -79,7 +79,7 @@ test('Can create a process and get the result', (done) => { restClient .createProcessInstanceWithResult({ processDefinitionKey, - variables: new myVariableDto({ someNumberField: 9 }), + variables: createDtoInstance(myVariableDto, { someNumberField: 9 }), }) .then((res) => { expect(res.processDefinitionKey).toEqual(processDefinitionKey) @@ -98,7 +98,7 @@ test('What happens if we time out?', async () => { await expect( restClient.createProcessInstanceWithResult({ bpmnProcessId, - variables: new myVariableDto({ someNumberField: 9 }), + variables: createDtoInstance(myVariableDto, { someNumberField: 9 }), requestTimeout: 20000, }) ).rejects.toThrow('504') diff --git a/src/__tests__/c8/rest/migrateProcess.rest.spec.ts b/src/__tests__/c8/rest/migrateProcess.rest.spec.ts index 9afce14b..5d8ee812 100644 --- a/src/__tests__/c8/rest/migrateProcess.rest.spec.ts +++ b/src/__tests__/c8/rest/migrateProcess.rest.spec.ts @@ -1,6 +1,6 @@ import path from 'path' -import { CamundaJobWorker } from 'c8/lib/C8JobWorker' +import { CamundaJobWorker } from 'c8/lib/CamundaJobWorker' import { CamundaRestClient } from '../../../c8/lib/CamundaRestClient' import { LosslessDto } from '../../../lib' diff --git a/src/admin/lib/AdminApiClient.ts b/src/admin/lib/AdminApiClient.ts index 0dd70fba..8a75f860 100644 --- a/src/admin/lib/AdminApiClient.ts +++ b/src/admin/lib/AdminApiClient.ts @@ -61,6 +61,7 @@ export class AdminApiClient { ), ], beforeError: [gotBeforeErrorHook], + beforeRequest: config.middleware ?? [], }, }) ) diff --git a/src/c8/index.ts b/src/c8/index.ts index d693f1ad..be7e61ef 100644 --- a/src/c8/index.ts +++ b/src/c8/index.ts @@ -59,60 +59,72 @@ export class Camunda8 { this.log = getLogger(config) } - public getOperateApiClient(): OperateApiClient { + public getOperateApiClient( + config: Camunda8ClientConfiguration = {} + ): OperateApiClient { if (!this.operateApiClient) { this.operateApiClient = new OperateApiClient({ - config: this.configuration, + config: { ...this.configuration, ...config }, oAuthProvider: this.oAuthProvider, }) } return this.operateApiClient } - public getAdminApiClient(): AdminApiClient { + public getAdminApiClient( + config: Camunda8ClientConfiguration = {} + ): AdminApiClient { if (!this.adminApiClient) { this.adminApiClient = new AdminApiClient({ - config: this.configuration, + config: { ...this.configuration, ...config }, oAuthProvider: this.oAuthProvider, }) } return this.adminApiClient } - public getModelerApiClient(): ModelerApiClient { + public getModelerApiClient( + config: Camunda8ClientConfiguration = {} + ): ModelerApiClient { if (!this.modelerApiClient) { this.modelerApiClient = new ModelerApiClient({ - config: this.configuration, + config: { ...this.configuration, ...config }, oAuthProvider: this.oAuthProvider, }) } return this.modelerApiClient } - public getOptimizeApiClient(): OptimizeApiClient { + public getOptimizeApiClient( + config: Camunda8ClientConfiguration = {} + ): OptimizeApiClient { if (!this.optimizeApiClient) { this.optimizeApiClient = new OptimizeApiClient({ - config: this.configuration, + config: { ...this.configuration, ...config }, oAuthProvider: this.oAuthProvider, }) } return this.optimizeApiClient } - public getTasklistApiClient(): TasklistApiClient { + public getTasklistApiClient( + config: Camunda8ClientConfiguration = {} + ): TasklistApiClient { if (!this.tasklistApiClient) { this.tasklistApiClient = new TasklistApiClient({ - config: this.configuration, + config: { ...this.configuration, ...config }, oAuthProvider: this.oAuthProvider, }) } return this.tasklistApiClient } - public getZeebeGrpcApiClient(): ZeebeGrpcClient { + public getZeebeGrpcApiClient( + config: Camunda8ClientConfiguration = {} + ): ZeebeGrpcClient { if (!this.zeebeGrpcApiClient) { this.zeebeGrpcApiClient = new ZeebeGrpcClient({ - config: this.configuration, + config: { ...this.configuration, ...config }, oAuthProvider: this.oAuthProvider, }) } @@ -122,20 +134,24 @@ export class Camunda8 { /** * @deprecated from 8.6.0. Please use getCamundaRestClient() instead. */ - public getZeebeRestClient(): ZeebeRestClient { + public getZeebeRestClient( + config: Camunda8ClientConfiguration = {} + ): ZeebeRestClient { if (!this.zeebeRestClient) { this.zeebeRestClient = new ZeebeRestClient({ - config: this.configuration, + config: { ...this.configuration, ...config }, oAuthProvider: this.oAuthProvider, }) } return this.zeebeRestClient } - public getCamundaRestClient(): CamundaRestClient { + public getCamundaRestClient( + config: Camunda8ClientConfiguration = {} + ): CamundaRestClient { if (!this.camundaRestClient) { this.camundaRestClient = new CamundaRestClient({ - config: this.configuration, + config: { ...this.configuration, ...config }, oAuthProvider: this.oAuthProvider, }) } diff --git a/src/c8/lib/CamundaRestClient.ts b/src/c8/lib/CamundaRestClient.ts index a62f0557..89cfc25b 100644 --- a/src/c8/lib/CamundaRestClient.ts +++ b/src/c8/lib/CamundaRestClient.ts @@ -138,6 +138,7 @@ export class CamundaRestClient { this.log.debug(`${method} ${path}`) this.log.silly(body) }, + ...(config.middleware ?? []), ], }, }) diff --git a/src/lib/Configuration.ts b/src/lib/Configuration.ts index 6bcf7292..e298c4d3 100644 --- a/src/lib/Configuration.ts +++ b/src/lib/Configuration.ts @@ -1,3 +1,4 @@ +import { BeforeRequestHook } from 'got' import mergeWith from 'lodash.mergewith' import { createEnv } from 'neon-env' import winston from 'winston' @@ -433,7 +434,9 @@ export class CamundaEnvironmentConfigurator { export type CamundaPlatform8Configuration = ReturnType< typeof CamundaEnvironmentConfigurator.ENV -> +> & { + middleware?: BeforeRequestHook[] +} export type DeepPartial = { [K in keyof T]?: T[K] extends object ? DeepPartial : T[K] diff --git a/src/modeler/lib/ModelerAPIClient.ts b/src/modeler/lib/ModelerAPIClient.ts index d052a22f..a5c0d103 100644 --- a/src/modeler/lib/ModelerAPIClient.ts +++ b/src/modeler/lib/ModelerAPIClient.ts @@ -56,6 +56,7 @@ export class ModelerApiClient { ), ], beforeError: [gotBeforeErrorHook], + beforeRequest: config.middleware ?? [], }, }) ) diff --git a/src/operate/lib/OperateApiClient.ts b/src/operate/lib/OperateApiClient.ts index 4833636c..f8da941f 100644 --- a/src/operate/lib/OperateApiClient.ts +++ b/src/operate/lib/OperateApiClient.ts @@ -111,6 +111,7 @@ export class OperateApiClient { ), ], beforeError: [gotBeforeErrorHook], + beforeRequest: config.middleware ?? [], }, }) ) diff --git a/src/optimize/lib/OptimizeApiClient.ts b/src/optimize/lib/OptimizeApiClient.ts index 29eb5d00..defea64a 100644 --- a/src/optimize/lib/OptimizeApiClient.ts +++ b/src/optimize/lib/OptimizeApiClient.ts @@ -89,6 +89,7 @@ export class OptimizeApiClient { ), ], beforeError: [gotBeforeErrorHook], + beforeRequest: config.middleware ?? [], }, }) ) diff --git a/src/tasklist/lib/TasklistApiClient.ts b/src/tasklist/lib/TasklistApiClient.ts index b4223545..cfdeaf0c 100644 --- a/src/tasklist/lib/TasklistApiClient.ts +++ b/src/tasklist/lib/TasklistApiClient.ts @@ -90,6 +90,7 @@ export class TasklistApiClient { ), ], beforeError: [gotBeforeErrorHook], + beforeRequest: config.middleware ?? [], }, }) ) From 926a49ca0f722b1359d378bb3e1718152e697f9c Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Fri, 11 Oct 2024 12:45:31 +1300 Subject: [PATCH 29/34] (chore): update changed API fields --- package-lock.json | 10 +++++++++ package.json | 1 + .../c8/rest/activateJobs.rest.spec.ts | 6 ++--- .../c8/rest/broadcastSigna.rest.spec.ts | 2 +- .../c8/rest/createProcess.rest.spec.ts | 10 ++++----- .../c8/rest/deleteResource.rest.spec.ts | 6 ++--- .../c8/rest/migrateProcess.rest.spec.ts | 2 +- src/__tests__/c8/rest/parseJobs.unit.spec.ts | 2 +- .../c8/rest/publishMessage.rest.spec.ts | 6 ++--- .../lib/LosslessJsonParser.unit.spec.ts | 2 +- .../Client-Update-Job-Timeout.spec.ts | 1 - .../Worker-Failure-Retries.spec.ts | 1 - .../zeebe/integration/Worker-Failure.spec.ts | 1 - .../OnConnectionError.spec.ts | 5 ----- src/c8/lib/C8Dto.ts | 22 +++++++++---------- src/c8/lib/CamundaRestClient.ts | 12 +++++----- src/lib/LosslessJsonParser.ts | 1 + src/zeebe/lib/GrpcClient.ts | 3 --- src/zeebe/lib/ZBWorkerBase.ts | 3 --- 19 files changed, 47 insertions(+), 49 deletions(-) diff --git a/package-lock.json b/package-lock.json index bfdae47d..58286e57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@grpc/grpc-js": "1.10.9", "@grpc/proto-loader": "0.7.13", + "@types/form-data": "^2.2.1", "chalk": "^2.4.2", "console-stamp": "^3.0.2", "dayjs": "^1.8.15", @@ -3692,6 +3693,15 @@ "@types/send": "*" } }, + "node_modules/@types/form-data": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", + "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "dev": true, diff --git a/package.json b/package.json index f5f3ee41..c9a59400 100644 --- a/package.json +++ b/package.json @@ -143,6 +143,7 @@ "dependencies": { "@grpc/grpc-js": "1.10.9", "@grpc/proto-loader": "0.7.13", + "@types/form-data": "^2.2.1", "chalk": "^2.4.2", "console-stamp": "^3.0.2", "dayjs": "^1.8.15", diff --git a/src/__tests__/c8/rest/activateJobs.rest.spec.ts b/src/__tests__/c8/rest/activateJobs.rest.spec.ts index 0a4133be..f9072b96 100644 --- a/src/__tests__/c8/rest/activateJobs.rest.spec.ts +++ b/src/__tests__/c8/rest/activateJobs.rest.spec.ts @@ -2,7 +2,7 @@ import path from 'node:path' import { CamundaRestClient } from '../../../c8/lib/CamundaRestClient' -let bpmnProcessId: string +let processDefinitionId: string const restClient = new CamundaRestClient() beforeAll(async () => { @@ -15,13 +15,13 @@ beforeAll(async () => { 'hello-world-complete-rest.bpmn' ), ]) - bpmnProcessId = res.processes[0].bpmnProcessId + processDefinitionId = res.processes[0].processDefinitionId }) test('Can service a task', (done) => { restClient .createProcessInstance({ - bpmnProcessId, + processDefinitionId, variables: { someNumberField: 8, }, diff --git a/src/__tests__/c8/rest/broadcastSigna.rest.spec.ts b/src/__tests__/c8/rest/broadcastSigna.rest.spec.ts index bddcc2c6..e9b6a2ac 100644 --- a/src/__tests__/c8/rest/broadcastSigna.rest.spec.ts +++ b/src/__tests__/c8/rest/broadcastSigna.rest.spec.ts @@ -29,7 +29,7 @@ test('Can start a process with a signal', async () => { }, }) - expect(res.key).toBeTruthy() + expect(res.signalKey).toBeTruthy() await new Promise((resolve) => { const w = c8.createJobWorker({ diff --git a/src/__tests__/c8/rest/createProcess.rest.spec.ts b/src/__tests__/c8/rest/createProcess.rest.spec.ts index 72c47e5d..7422dd1e 100644 --- a/src/__tests__/c8/rest/createProcess.rest.spec.ts +++ b/src/__tests__/c8/rest/createProcess.rest.spec.ts @@ -5,7 +5,7 @@ import { createDtoInstance, LosslessDto } from '../../../lib' jest.setTimeout(17000) -let bpmnProcessId: string +let processDefinitionId: string let processDefinitionKey: string const restClient = new CamundaRestClient() @@ -13,7 +13,7 @@ beforeAll(async () => { const res = await restClient.deployResourcesFromFiles([ path.join('.', 'src', '__tests__', 'testdata', 'create-process-rest.bpmn'), ]) - ;({ bpmnProcessId, processDefinitionKey } = res.processes[0]) + ;({ processDefinitionId, processDefinitionKey } = res.processes[0]) }) class myVariableDto extends LosslessDto { @@ -23,7 +23,7 @@ class myVariableDto extends LosslessDto { test('Can create a process from bpmn id', (done) => { restClient .createProcessInstance({ - bpmnProcessId, + processDefinitionId, variables: { someNumberField: 8, }, @@ -94,10 +94,10 @@ test('What happens if we time out?', async () => { const res = await restClient.deployResourcesFromFiles([ path.join('.', 'src', '__tests__', 'testdata', 'hello-world-complete.bpmn'), ]) - const bpmnProcessId = res.processes[0].bpmnProcessId + const processDefinitionId = res.processes[0].processDefinitionId await expect( restClient.createProcessInstanceWithResult({ - bpmnProcessId, + processDefinitionId, variables: createDtoInstance(myVariableDto, { someNumberField: 9 }), requestTimeout: 20000, }) diff --git a/src/__tests__/c8/rest/deleteResource.rest.spec.ts b/src/__tests__/c8/rest/deleteResource.rest.spec.ts index 175f03a9..a00b2d4f 100644 --- a/src/__tests__/c8/rest/deleteResource.rest.spec.ts +++ b/src/__tests__/c8/rest/deleteResource.rest.spec.ts @@ -7,15 +7,15 @@ test('It can delete a resource', async () => { './src/__tests__/testdata/Delete-Resource-Rest.bpmn', ]) const key = res.processes[0].processDefinitionKey - const id = res.processes[0].bpmnProcessId + const id = res.processes[0].processDefinitionId const wfi = await c8.createProcessInstance({ - bpmnProcessId: id, + processDefinitionId: id, variables: {}, }) expect(wfi.processDefinitionKey).toBe(key) await c8.deleteResource({ resourceKey: key }) // After deleting the process definition, we should not be able to start a new process instance. await expect( - c8.createProcessInstance({ bpmnProcessId: id, variables: {} }) + c8.createProcessInstance({ processDefinitionId: id, variables: {} }) ).rejects.toThrow('404') }) diff --git a/src/__tests__/c8/rest/migrateProcess.rest.spec.ts b/src/__tests__/c8/rest/migrateProcess.rest.spec.ts index 5d8ee812..95642609 100644 --- a/src/__tests__/c8/rest/migrateProcess.rest.spec.ts +++ b/src/__tests__/c8/rest/migrateProcess.rest.spec.ts @@ -25,7 +25,7 @@ test('RestClient can migrate a process instance', async () => { // Create an instance of the process model const processInstance = await c8.createProcessInstance({ - bpmnProcessId: 'migrant-work-rest', + processDefinitionId: 'migrant-work-rest', variables: {}, }) diff --git a/src/__tests__/c8/rest/parseJobs.unit.spec.ts b/src/__tests__/c8/rest/parseJobs.unit.spec.ts index 60d4d9e9..07c35a6f 100644 --- a/src/__tests__/c8/rest/parseJobs.unit.spec.ts +++ b/src/__tests__/c8/rest/parseJobs.unit.spec.ts @@ -15,7 +15,7 @@ class CustomHeaders extends LosslessDto { const myJob = createSpecializedRestApiJobClass(Variables, CustomHeaders) test('It correctly parses variables and custom headers', () => { - const jsonString = `{"jobs":[{"key":2251799813737371,"type":"console-log-complete","processInstanceKey":2251799813737366,"bpmnProcessId":"hello-world-complete","processDefinitionVersion":1,"processDefinitionKey":2251799813736299,"elementId":"ServiceTask_0g6tf5f","elementInstanceKey":2251799813737370,"customHeaders":{"message":"Hello World","bigHeader":1,"smallHeader":2},"worker":"test","retries":100,"deadline":1725501895792,"variables":{"bigValue":3},"tenantId":""}]}` + const jsonString = `{"jobs":[{"key":2251799813737371,"type":"console-log-complete","processInstanceKey":2251799813737366,"processDefinitionId":"hello-world-complete","processDefinitionVersion":1,"processDefinitionKey":2251799813736299,"elementId":"ServiceTask_0g6tf5f","elementInstanceKey":2251799813737370,"customHeaders":{"message":"Hello World","bigHeader":1,"smallHeader":2},"worker":"test","retries":100,"deadline":1725501895792,"variables":{"bigValue":3},"tenantId":""}]}` const res = losslessParse(jsonString, myJob, 'jobs') expect(res[0].variables.bigValue).toBe('3') expect(res[0].customHeaders.smallHeader).toBe(2) diff --git a/src/__tests__/c8/rest/publishMessage.rest.spec.ts b/src/__tests__/c8/rest/publishMessage.rest.spec.ts index 59f5f57d..c6594a00 100644 --- a/src/__tests__/c8/rest/publishMessage.rest.spec.ts +++ b/src/__tests__/c8/rest/publishMessage.rest.spec.ts @@ -17,7 +17,7 @@ test('Can publish a message', (done) => { messageReceived!: boolean } c8.createProcessInstanceWithResult({ - bpmnProcessId: 'rest-message-test', + processDefinitionId: 'rest-message-test', variables: { correlationId: uuid, }, @@ -43,7 +43,7 @@ test('Can correlate a message', (done) => { messageReceived!: boolean } c8.createProcessInstanceWithResult({ - bpmnProcessId: 'rest-message-test', + processDefinitionId: 'rest-message-test', variables: { correlationId: uuid, }, @@ -69,7 +69,7 @@ test('Correlate message returns expected data', (done) => { const uuid = v4() let processInstanceKey: string c8.createProcessInstance({ - bpmnProcessId: 'rest-message-test', + processDefinitionId: 'rest-message-test', variables: { correlationId: uuid, }, diff --git a/src/__tests__/lib/LosslessJsonParser.unit.spec.ts b/src/__tests__/lib/LosslessJsonParser.unit.spec.ts index 934254ec..7906413d 100644 --- a/src/__tests__/lib/LosslessJsonParser.unit.spec.ts +++ b/src/__tests__/lib/LosslessJsonParser.unit.spec.ts @@ -349,7 +349,7 @@ test('LosslessStringify correctly handles null objects', () => { }) test('LosslessJsonParser handles subkeys', () => { - const jsonString = `{"jobs":[{"key":2251799813737371,"type":"console-log-complete","processInstanceKey":2251799813737366,"bpmnProcessId":"hello-world-complete","processDefinitionVersion":1,"processDefinitionKey":2251799813736299,"elementId":"ServiceTask_0g6tf5f","elementInstanceKey":2251799813737370,"customHeaders":{"message":"Hello World"},"worker":"test","retries":100,"deadline":1725501895792,"variables":{},"tenantId":""}]}` + const jsonString = `{"jobs":[{"key":2251799813737371,"type":"console-log-complete","processInstanceKey":2251799813737366,"processDefinitionId":"hello-world-complete","processDefinitionVersion":1,"processDefinitionKey":2251799813736299,"elementId":"ServiceTask_0g6tf5f","elementInstanceKey":2251799813737370,"customHeaders":{"message":"Hello World"},"worker":"test","retries":100,"deadline":1725501895792,"variables":{},"tenantId":""}]}` const parsed = losslessParse(jsonString, undefined, 'jobs') expect(parsed[0].key).toBe(2251799813737371) diff --git a/src/__tests__/zeebe/integration/Client-Update-Job-Timeout.spec.ts b/src/__tests__/zeebe/integration/Client-Update-Job-Timeout.spec.ts index 2584354e..12b53ddd 100644 --- a/src/__tests__/zeebe/integration/Client-Update-Job-Timeout.spec.ts +++ b/src/__tests__/zeebe/integration/Client-Update-Job-Timeout.spec.ts @@ -22,7 +22,6 @@ afterEach(async () => { await zbc.cancelProcessInstance(wf.processInstanceKey) } } catch (e: unknown) { - // console.log('Caught NOT FOUND') // @DEBUG } finally { await zbc.close() // Makes sure we don't forget to close connection } diff --git a/src/__tests__/zeebe/integration/Worker-Failure-Retries.spec.ts b/src/__tests__/zeebe/integration/Worker-Failure-Retries.spec.ts index f47b3ae2..27ec4f58 100644 --- a/src/__tests__/zeebe/integration/Worker-Failure-Retries.spec.ts +++ b/src/__tests__/zeebe/integration/Worker-Failure-Retries.spec.ts @@ -22,7 +22,6 @@ afterEach(async () => { await zbc.cancelProcessInstance(wf.processInstanceKey) } } catch (e: unknown) { - // console.log('Caught NOT FOUND') // @DEBUG } finally { await zbc.close() // Makes sure we don't forget to close connection } diff --git a/src/__tests__/zeebe/integration/Worker-Failure.spec.ts b/src/__tests__/zeebe/integration/Worker-Failure.spec.ts index e018eea2..041c7463 100644 --- a/src/__tests__/zeebe/integration/Worker-Failure.spec.ts +++ b/src/__tests__/zeebe/integration/Worker-Failure.spec.ts @@ -58,7 +58,6 @@ afterEach(async () => { await zbc.cancelProcessInstance(wf.processInstanceKey) } } catch (e: unknown) { - // console.log('Caught NOT FOUND') // @DEBUG } }) diff --git a/src/__tests__/zeebe/local-integration/OnConnectionError.spec.ts b/src/__tests__/zeebe/local-integration/OnConnectionError.spec.ts index 0f50094c..87a74505 100644 --- a/src/__tests__/zeebe/local-integration/OnConnectionError.spec.ts +++ b/src/__tests__/zeebe/local-integration/OnConnectionError.spec.ts @@ -132,11 +132,6 @@ xtest('Does not call the onConnectionError handler if there is a business error' let wf = 'arstsrasrateiuhrastulyharsntharsie' const zbc2 = new ZeebeGrpcClient() zbc2.on('connectionError', () => { - // tslint:disable-next-line: no-console - // console.log('OnConnectionError!!!! Incrementing calledF') // @DEBUG - // const e = new Error() - // tslint:disable-next-line: no-console - // console.log(e.stack) // @DEBUG calledF++ }) diff --git a/src/c8/lib/C8Dto.ts b/src/c8/lib/C8Dto.ts index 21fce315..fae31d4f 100644 --- a/src/c8/lib/C8Dto.ts +++ b/src/c8/lib/C8Dto.ts @@ -12,7 +12,7 @@ export class RestApiJob< type!: string @Int64String processInstanceKey!: string - bpmnProcessId!: string + processDefinitionId!: string processDefinitionVersion!: number @Int64String processDefinitionKey!: string @@ -63,8 +63,8 @@ export interface NewUserInfo { export type Ctor = new (obj: any) => T export class ProcessDeployment extends LosslessDto { - bpmnProcessId!: string - version!: number + processDefinitionId!: string + processDefinitionVersion!: number @Int64String processDefinitionKey!: string resourceName!: string @@ -103,10 +103,10 @@ export class FormDeployment { export class DeployResourceResponseDto extends LosslessDto { @Int64String - key!: string + deploymentKey!: string deployments!: ( - | { process: ProcessDeployment } - | { decision: DecisionDeployment } + | { processDefinition: ProcessDeployment } + | { decisionDefinition: DecisionDeployment } | { decisionRequirements: DecisionRequirementsDeployment } | { form: FormDeployment } )[] @@ -130,7 +130,7 @@ export class CreateProcessInstanceResponse> { /** * The BPMN process ID of the process definition */ - readonly bpmnProcessId!: string + readonly processDefinitionId!: string /** * The version of the process; set to -1 to use the latest version */ @@ -170,7 +170,7 @@ export interface MigrationRequest { export class BroadcastSignalResponse extends LosslessDto { @Int64String /** The unique ID of the signal that was broadcast. */ - key!: string + signalKey!: string /** The tenant ID of the signal that was broadcast. */ tenantId!: string } @@ -259,13 +259,13 @@ export interface ProcessInstanceCreationStartInstruction { elementId: string } -export interface CreateProcessInstanceFromBpmnProcessId< +export interface CreateProcessInstanceFromProcessDefinitionId< V extends JSONDoc | LosslessDto, > extends CreateProcessBaseRequest { /** * the BPMN process ID of the process definition */ - bpmnProcessId: string + processDefinitionId: string } export interface CreateProcessInstanceFromProcessDefinition< @@ -278,7 +278,7 @@ export interface CreateProcessInstanceFromProcessDefinition< } export type CreateProcessInstanceReq = - | CreateProcessInstanceFromBpmnProcessId + | CreateProcessInstanceFromProcessDefinitionId | CreateProcessInstanceFromProcessDefinition export interface PatchAuthorizationRequest { diff --git a/src/c8/lib/CamundaRestClient.ts b/src/c8/lib/CamundaRestClient.ts index 89cfc25b..9ae28cad 100644 --- a/src/c8/lib/CamundaRestClient.ts +++ b/src/c8/lib/CamundaRestClient.ts @@ -760,7 +760,7 @@ export class CamundaRestClient { * and re-parsing each of the deployments with the correct Dto. */ const deploymentResponse = new DeployResourceResponse() - deploymentResponse.key = res.key.toString() + deploymentResponse.deploymentKey = res.deploymentKey.toString() deploymentResponse.tenantId = res.tenantId deploymentResponse.deployments = [] deploymentResponse.processes = [] @@ -773,10 +773,10 @@ export class CamundaRestClient { */ const isProcessDeployment = ( deployment - ): deployment is { process: ProcessDeployment } => !!deployment.process + ): deployment is { processDefinition: ProcessDeployment } => !!deployment.processDefinition const isDecisionDeployment = ( deployment - ): deployment is { decision: DecisionDeployment } => !!deployment.decision + ): deployment is { decisionDefinition: DecisionDeployment } => !!deployment.decisionDefinition const isDecisionRequirementsDeployment = ( deployment ): deployment is { decisionRequirements: DecisionRequirementsDeployment } => @@ -793,10 +793,10 @@ export class CamundaRestClient { res.deployments.forEach((deployment) => { if (isProcessDeployment(deployment)) { const processDeployment = losslessParse( - stringify(deployment.process)!, + stringify(deployment.processDefinition)!, ProcessDeployment ) - deploymentResponse.deployments.push({ process: processDeployment }) + deploymentResponse.deployments.push({ processDefinition: processDeployment }) deploymentResponse.processes.push(processDeployment) } if (isDecisionDeployment(deployment)) { @@ -804,7 +804,7 @@ export class CamundaRestClient { stringify(deployment)!, DecisionDeployment ) - deploymentResponse.deployments.push({ decision: decisionDeployment }) + deploymentResponse.deployments.push({ decisionDefinition: decisionDeployment }) deploymentResponse.decisions.push(decisionDeployment) } if (isDecisionRequirementsDeployment(deployment)) { diff --git a/src/lib/LosslessJsonParser.ts b/src/lib/LosslessJsonParser.ts index 4486f13b..336a6a69 100644 --- a/src/lib/LosslessJsonParser.ts +++ b/src/lib/LosslessJsonParser.ts @@ -202,6 +202,7 @@ export function losslessParse( * This way we lose no fidelity at this stage, and can then use a supplied DTO to map large numbers * or throw if we find an unsafe number. */ + const parsedLossless = parse(json) as any /** diff --git a/src/zeebe/lib/GrpcClient.ts b/src/zeebe/lib/GrpcClient.ts index 1c6c92e2..57a92df3 100644 --- a/src/zeebe/lib/GrpcClient.ts +++ b/src/zeebe/lib/GrpcClient.ts @@ -504,9 +504,6 @@ export class GrpcClient extends EventEmitter { }) return setTimeout(() => { - // tslint:disable-next-line: no-console - console.log(`Channel timeout after ${timeout}`) // @DEBUG - return isClosed(this.channelState) ? null : reject(new Error(`Didn't close in time: ${this.channelState}`)) diff --git a/src/zeebe/lib/ZBWorkerBase.ts b/src/zeebe/lib/ZBWorkerBase.ts index efdbe112..0ec7da80 100644 --- a/src/zeebe/lib/ZBWorkerBase.ts +++ b/src/zeebe/lib/ZBWorkerBase.ts @@ -301,9 +301,6 @@ export class ZBWorkerBase< chalk.red(`WARNING: Call to ${thisMethod}() after ${methodCalled}() was called. You should call only one job action method in the worker handler. This is a bug in the ${this.taskType} worker handler.`) ) - // tslint:disable-next-line: no-console - console.log('handler', this.taskHandler.toString()) // @DEBUG - return wrappedFunction(...args) } methodCalled = thisMethod From e3d92069a83c0c20635c99549a45230e35820aae Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Tue, 22 Oct 2024 14:51:10 -0500 Subject: [PATCH 30/34] chore: make logger generic --- src/c8/index.ts | 6 ++--- src/c8/lib/C8Dto.ts | 48 +++++++++++++++++++++++++++++++-- src/c8/lib/C8Logger.ts | 32 +++++++++++++++------- src/c8/lib/CamundaJobWorker.ts | 27 ++++++++++--------- src/c8/lib/CamundaRestClient.ts | 41 +++++++++++++++------------- src/lib/Configuration.ts | 10 +++---- 6 files changed, 112 insertions(+), 52 deletions(-) diff --git a/src/c8/index.ts b/src/c8/index.ts index be7e61ef..64ac0050 100644 --- a/src/c8/index.ts +++ b/src/c8/index.ts @@ -1,5 +1,3 @@ -import winston from 'winston' - import { AdminApiClient } from '../admin' import { Camunda8ClientConfiguration, @@ -14,7 +12,7 @@ import { OptimizeApiClient } from '../optimize' import { TasklistApiClient } from '../tasklist' import { ZeebeGrpcClient, ZeebeRestClient } from '../zeebe' -import { getLogger } from './lib/C8Logger' +import { getLogger, Logger } from './lib/C8Logger' import { CamundaRestClient } from './lib/CamundaRestClient' /** @@ -47,7 +45,7 @@ export class Camunda8 { private configuration: CamundaPlatform8Configuration private oAuthProvider: IOAuthProvider private camundaRestClient?: CamundaRestClient - public log: winston.Logger + public log: Logger /** * All constructor parameters for configuration are optional. If no configuration is provided, the SDK will use environment variables to configure itself. diff --git a/src/c8/lib/C8Dto.ts b/src/c8/lib/C8Dto.ts index fae31d4f..b09278c1 100644 --- a/src/c8/lib/C8Dto.ts +++ b/src/c8/lib/C8Dto.ts @@ -1,14 +1,14 @@ import { LosslessNumber } from 'lossless-json' import { Int64String, LosslessDto } from '../../lib' -import { JSONDoc } from '../../zeebe/types' +import { ICustomHeaders, IInputVariables, JSONDoc } from '../../zeebe/types' export class RestApiJob< Variables = LosslessDto, CustomHeaders = LosslessDto, > extends LosslessDto { @Int64String - key!: string + jobKey!: string type!: string @Int64String processInstanceKey!: string @@ -309,3 +309,47 @@ export interface PatchAuthorizationRequest { resourceIds: [] }[] } + +export interface RestJob< + Variables = IInputVariables, + CustomHeaderShape = ICustomHeaders, +> { + /** The key, a unique identifier for the job */ + readonly jobKey: string + /** + * The job type, as defined in the BPMN process (e.g. ) + */ + readonly type: string + /** The job's process instance key */ + readonly processInstanceKey: string + /** The bpmn process ID of the job process definition */ + readonly bpmnProcessId: string + /** The version of the job process definition */ + readonly processDefinitionVersion: number + /** The associated task element ID */ + readonly elementId: string + /** + * The unique key identifying the associated task, unique within the scope of the + * process instance + */ + readonly elementInstanceKey: string + /** + * A set of custom headers defined during modelling + */ + readonly customHeaders: Readonly + /** The name of the worker that activated this job */ + readonly worker: string + /* The amount of retries left to this job (should always be positive) */ + readonly retries: number + /** Epoch milliseconds */ + readonly deadline: string + /** + * All visible variables in the task scope, computed at activation time. + */ + readonly variables: Readonly + /** + * The `tenantId` of the job in a multi-tenant cluster + */ + readonly tenantId: string +} diff --git a/src/c8/lib/C8Logger.ts b/src/c8/lib/C8Logger.ts index fe3111fe..03b022d2 100644 --- a/src/c8/lib/C8Logger.ts +++ b/src/c8/lib/C8Logger.ts @@ -5,8 +5,18 @@ import { CamundaEnvironmentConfigurator, } from '../../lib' -let defaultLogger: winston.Logger -let cachedLogger: winston.Logger | undefined +export type Logger = { + /* eslint-disable @typescript-eslint/no-explicit-any */ + info: (message: string | undefined, ...meta: any[]) => void + warn: (message: string | undefined, ...meta: any[]) => void + error: (message: string | undefined, ...meta: any[]) => void + debug: (message: string | undefined, ...meta: any[]) => void + trace: (message: string | undefined, ...meta: any[]) => void + /* eslint-enable @typescript-eslint/no-explicit-any */ +} + +let defaultLogger: Logger +let cachedLogger: Logger | undefined export function getLogger(config?: Camunda8ClientConfiguration) { const configuration = @@ -14,13 +24,14 @@ export function getLogger(config?: Camunda8ClientConfiguration) { // We assume that the SDK user uses a single winston instance for 100% of logging, or no logger at all (in which case we create our own) if (config?.logger && cachedLogger !== config.logger) { cachedLogger = config.logger - config.logger.info( - `Using supplied winston logger at level '${config.logger.level}'` - ) + config.logger.debug(`Using supplied logger`) } if (!defaultLogger) { // Define the default logger - defaultLogger = winston.createLogger({ + const logger: winston.Logger & { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + trace: (message: string | undefined, ...meta: any[]) => void + } = winston.createLogger({ level: configuration.CAMUNDA_LOG_LEVEL, format: winston.format.combine( winston.format.timestamp(), @@ -28,12 +39,13 @@ export function getLogger(config?: Camunda8ClientConfiguration) { winston.format.simple() ), transports: [new winston.transports.Console()], - }) + }) as any // eslint-disable-line @typescript-eslint/no-explicit-any + + logger.trace = logger.silly + defaultLogger = logger } if (!cachedLogger) { - defaultLogger.info( - `Using default winston logger at level '${defaultLogger.level}'` - ) + defaultLogger.debug(`Using default winston logger`) cachedLogger = defaultLogger } return config?.logger ?? defaultLogger diff --git a/src/c8/lib/CamundaJobWorker.ts b/src/c8/lib/CamundaJobWorker.ts index dfa417a9..c41254f7 100644 --- a/src/c8/lib/CamundaJobWorker.ts +++ b/src/c8/lib/CamundaJobWorker.ts @@ -1,19 +1,17 @@ import { EventEmitter } from 'events' import TypedEmitter from 'typed-emitter' -import winston from 'winston' import { LosslessDto } from '../../lib' import { ActivateJobsRequest, IProcessVariables, - Job, JobCompletionInterfaceRest, MustReturnJobActionAcknowledgement, } from '../../zeebe/types' -import { Ctor } from './C8Dto' -import { getLogger } from './C8Logger' +import { Ctor, RestJob } from './C8Dto' +import { getLogger, Logger } from './C8Logger' import { CamundaRestClient } from './CamundaRestClient' type CamundaJobWorkerEvents = { @@ -40,11 +38,11 @@ export interface CamundaJobWorkerConfig< /** How often the worker will poll for new jobs. Defaults to 30s */ pollIntervalMs?: number jobHandler: ( - job: Job & + job: RestJob & JobCompletionInterfaceRest, - log: winston.Logger + log: Logger ) => MustReturnJobActionAcknowledgement - logger?: winston.Logger + logger?: Logger /** Default: true. Start the worker polling immediately. If set to `false`, call the worker's `start()` method to start polling for work. */ autoStart?: boolean } @@ -57,7 +55,7 @@ export class CamundaJobWorker< public capacity: number private loopHandle?: NodeJS.Timeout private pollInterval: number - public log: winston.Logger + public log: Logger logMeta: () => { worker: string type: string @@ -147,7 +145,7 @@ export class CamundaJobWorker< return } - this.log.silly(`Polling for jobs`, this.logMeta()) + this.log.trace(`Polling for jobs`, this.logMeta()) const remainingJobCapacity = this.config.maxJobsToActivate - this.currentlyActiveJobCount @@ -167,14 +165,17 @@ export class CamundaJobWorker< } private async handleJob( - job: Job & + job: RestJob & JobCompletionInterfaceRest ) { try { - this.log.debug(`Invoking job handler for job ${job.key}`, this.logMeta()) + this.log.debug( + `Invoking job handler for job ${job.jobKey}`, + this.logMeta() + ) await this.config.jobHandler(job, this.log) this.log.debug( - `Completed job handler for job ${job.key}.`, + `Completed job handler for job ${job.jobKey}.`, this.logMeta() ) } catch (e) { @@ -182,7 +183,7 @@ export class CamundaJobWorker< if (e instanceof Error) { // If err is an instance of Error, we can safely access its properties this.log.error( - `Unhandled exception in job handler for job ${job.key}`, + `Unhandled exception in job handler for job ${job.jobKey}`, this.logMeta() ) this.log.error(`Error: ${e.message}`, { diff --git a/src/c8/lib/CamundaRestClient.ts b/src/c8/lib/CamundaRestClient.ts index 9ae28cad..438814d7 100644 --- a/src/c8/lib/CamundaRestClient.ts +++ b/src/c8/lib/CamundaRestClient.ts @@ -4,7 +4,6 @@ import { debug } from 'debug' import FormData from 'form-data' import got from 'got' import { parse, stringify } from 'lossless-json' -import winston from 'winston' import { Camunda8ClientConfiguration, @@ -29,7 +28,6 @@ import { ErrorJobWithVariables, FailJobRequest, IProcessVariables, - Job, JOB_ACTION_ACKNOWLEDGEMENT, JobCompletionInterfaceRest, JSONDoc, @@ -54,10 +52,11 @@ import { PatchAuthorizationRequest, ProcessDeployment, PublishMessageResponse, + RestJob, TaskChangeSet, UpdateElementVariableRequest, } from './C8Dto' -import { getLogger } from './C8Logger' +import { getLogger, Logger } from './C8Logger' import { CamundaJobWorker, CamundaJobWorkerConfig } from './CamundaJobWorker' import { createSpecializedRestApiJobClass } from './RestApiJobClassFactory' import { createSpecializedCreateProcessInstanceResponseClass } from './RestApiProcessInstanceClassFactory' @@ -77,14 +76,14 @@ class DefaultLosslessDto extends LosslessDto {} * `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 - * + * @experimental this API may be removed from this package in a future version, and moved to an ESM package. Can you use ESM in your project? Comment [on this issue](https://github.com/camunda/camunda-8-js-sdk/issues/267). */ export class CamundaRestClient { private userAgentString: string private oAuthProvider: IOAuthProvider private rest: Promise private tenantId?: string - public log: winston.Logger + public log: Logger /** * All constructor parameters for configuration are optional. If no configuration is provided, the SDK will use environment variables to configure itself. @@ -97,7 +96,7 @@ export class CamundaRestClient { options?.config ?? {} ) this.log = getLogger(config) - this.log.info(`Using REST API version ${CAMUNDA_REST_API_VERSION}`) + this.log.debug(`Using REST API version ${CAMUNDA_REST_API_VERSION}`) trace('options.config', options?.config) trace('config', config) this.oAuthProvider = @@ -136,7 +135,7 @@ export class CamundaRestClient { trace(`${method} ${path}`) trace(body) this.log.debug(`${method} ${path}`) - this.log.silly(body) + this.log.trace(body?.toString()) }, ...(config.middleware ?? []), ], @@ -443,7 +442,7 @@ export class CamundaRestClient { customHeadersDto?: Ctor } ): Promise< - (Job & + (RestJob & JobCompletionInterfaceRest)[] > { const headers = await this.getHeaders() @@ -475,7 +474,7 @@ export class CamundaRestClient { headers, parseJson: (text) => losslessParse(text, jobDto, 'jobs'), }) - .json[]>() + .json[]>() .then((activatedJobs) => activatedJobs.map(this.addJobMethods)) ) } @@ -773,10 +772,12 @@ export class CamundaRestClient { */ const isProcessDeployment = ( deployment - ): deployment is { processDefinition: ProcessDeployment } => !!deployment.processDefinition + ): deployment is { processDefinition: ProcessDeployment } => + !!deployment.processDefinition const isDecisionDeployment = ( deployment - ): deployment is { decisionDefinition: DecisionDeployment } => !!deployment.decisionDefinition + ): deployment is { decisionDefinition: DecisionDeployment } => + !!deployment.decisionDefinition const isDecisionRequirementsDeployment = ( deployment ): deployment is { decisionRequirements: DecisionRequirementsDeployment } => @@ -796,7 +797,9 @@ export class CamundaRestClient { stringify(deployment.processDefinition)!, ProcessDeployment ) - deploymentResponse.deployments.push({ processDefinition: processDeployment }) + deploymentResponse.deployments.push({ + processDefinition: processDeployment, + }) deploymentResponse.processes.push(processDeployment) } if (isDecisionDeployment(deployment)) { @@ -804,7 +807,9 @@ export class CamundaRestClient { stringify(deployment)!, DecisionDeployment ) - deploymentResponse.deployments.push({ decisionDefinition: decisionDeployment }) + deploymentResponse.deployments.push({ + decisionDefinition: decisionDeployment, + }) deploymentResponse.decisions.push(decisionDeployment) } if (isDecisionRequirementsDeployment(deployment)) { @@ -927,8 +932,8 @@ export class CamundaRestClient { } private addJobMethods = ( - job: Job - ): Job & + job: RestJob + ): RestJob & JobCompletionInterfaceRest => { return { ...job, @@ -937,19 +942,19 @@ export class CamundaRestClient { }, complete: (variables: IProcessVariables = {}) => this.completeJob({ - jobKey: job.key, + jobKey: job.jobKey, variables, }), error: (error) => this.errorJob({ ...error, - jobKey: job.key, + jobKey: job.jobKey, }), fail: (failJobRequest) => this.failJob(failJobRequest), /* This has an effect in a Job Worker, decrementing the currently active job count */ forward: () => JOB_ACTION_ACKNOWLEDGEMENT, modifyJobTimeout: ({ newTimeoutMs }: { newTimeoutMs: number }) => - this.updateJob({ jobKey: job.key, timeout: newTimeoutMs }), + this.updateJob({ jobKey: job.jobKey, timeout: newTimeoutMs }), } } diff --git a/src/lib/Configuration.ts b/src/lib/Configuration.ts index e298c4d3..60f6893d 100644 --- a/src/lib/Configuration.ts +++ b/src/lib/Configuration.ts @@ -1,7 +1,8 @@ import { BeforeRequestHook } from 'got' import mergeWith from 'lodash.mergewith' import { createEnv } from 'neon-env' -import winston from 'winston' + +import { Logger } from '../c8/lib/C8Logger' const getMainEnv = () => createEnv({ @@ -366,9 +367,8 @@ const getEnv = () => ({ // Helper type for enforcing array contents to match an object's keys // eslint-disable-next-line @typescript-eslint/no-explicit-any -type EnforceArrayContent = T extends Array - ? T - : never +type EnforceArrayContent = + T extends Array ? T : never // Function to create a complete keys array, enforcing completeness at compile time // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -444,5 +444,5 @@ export type DeepPartial = { export type Camunda8ClientConfiguration = DeepPartial & { - logger?: winston.Logger + logger?: Logger } From 44d041d87c0db99f0aab0a441c967bcec5561fba Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Wed, 23 Oct 2024 14:35:04 -0500 Subject: [PATCH 31/34] chore: add note to new Camunda REST client --- package-lock.json | 23 ++- package.json | 1 + .../GetCustomCertificateBuffer.unit.spec.ts | 154 ++++++++++-------- src/c8/lib/CamundaRestClient.ts | 2 +- 4 files changed, 100 insertions(+), 80 deletions(-) diff --git a/package-lock.json b/package-lock.json index 58286e57..c2f33c02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,7 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-prettier": "^5.0.1", "express": "^4.19.2", + "get-port-please": "^3.1.2", "grpc-tools": "^1.12.4", "husky": "^8.0.3", "jest": "^29.7.0", @@ -3517,6 +3518,16 @@ "tar-fs": "^2.0.0" } }, + "node_modules/@sitapati/testcontainers/node_modules/get-port": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-4.2.0.tgz", + "integrity": "sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "license": "MIT", @@ -8159,14 +8170,12 @@ "node": ">=8.0.0" } }, - "node_modules/get-port": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-4.2.0.tgz", - "integrity": "sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw==", + "node_modules/get-port-please": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.1.2.tgz", + "integrity": "sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==", "dev": true, - "engines": { - "node": ">=6" - } + "license": "MIT" }, "node_modules/get-stream": { "version": "5.2.0", diff --git a/package.json b/package.json index c9a59400..de3ac5ab 100644 --- a/package.json +++ b/package.json @@ -119,6 +119,7 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-prettier": "^5.0.1", "express": "^4.19.2", + "get-port-please": "^3.1.2", "grpc-tools": "^1.12.4", "husky": "^8.0.3", "jest": "^29.7.0", diff --git a/src/__tests__/lib/GetCustomCertificateBuffer.unit.spec.ts b/src/__tests__/lib/GetCustomCertificateBuffer.unit.spec.ts index 481d5b67..eba2b756 100644 --- a/src/__tests__/lib/GetCustomCertificateBuffer.unit.spec.ts +++ b/src/__tests__/lib/GetCustomCertificateBuffer.unit.spec.ts @@ -5,6 +5,7 @@ import path from 'path' import { loadPackageDefinition, Server, ServerCredentials } from '@grpc/grpc-js' import { loadSync } from '@grpc/proto-loader' import express from 'express' +import { getPort } from 'get-port-please' import { BrokerInfo, @@ -82,86 +83,95 @@ test('Can use a custom root certificate to connect to a REST API', async () => { server.close() }) -test('gRPC server with self-signed certificate', (done) => { - // Load the protobuf definition - const packageDefinition = loadSync( - path.join(__dirname, '..', '..', 'proto', 'zeebe.proto'), - { - keepCase: true, - longs: String, - enums: String, - defaults: true, - oneofs: true, - } - ) +test('gRPC server with self-signed certificate', async () => { + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve) => { + // Load the protobuf definition + const packageDefinition = loadSync( + path.join(__dirname, '..', '..', 'proto', 'zeebe.proto'), + { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, + } + ) - const zeebeProto = loadPackageDefinition( - packageDefinition - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ) as unknown as { gateway_protocol: { Gateway: any } } + const zeebeProto = loadPackageDefinition( + packageDefinition + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ) as unknown as { gateway_protocol: { Gateway: any } } - // Create the server - server = new Server() + // Create the server + server = new Server() - // Add a service to the server - server.addService(zeebeProto.gateway_protocol.Gateway.service, { - Topology: (_, callback) => { - const t = new TopologyResponse() - const b = new BrokerInfo() - b.setHost('localhost') - const partition = new Partition() - partition.setHealth(0) - partition.setPartitionid(0) - partition.setRole(0) - b.setPartitionsList([partition]) - t.setBrokersList([b]) - callback(null, t) - }, - // Implement your service methods here - }) + // Add a service to the server + server.addService(zeebeProto.gateway_protocol.Gateway.service, { + Topology: (_, callback) => { + const t = new TopologyResponse() + const b = new BrokerInfo() + b.setHost('localhost') + const partition = new Partition() + partition.setHealth(0) + partition.setPartitionid(0) + partition.setRole(0) + b.setPartitionsList([partition]) + t.setBrokersList([b]) + callback(null, t) + }, + // Implement your service methods here + }) - // Read the key and certificate - const key = fs.readFileSync(path.join(__dirname, 'localhost.key')) - const cert = fs.readFileSync(path.join(__dirname, 'localhost.crt')) + // Read the key and certificate + const key = fs.readFileSync(path.join(__dirname, 'localhost.key')) + const cert = fs.readFileSync(path.join(__dirname, 'localhost.crt')) - // Start the server - server.bindAsync( - 'localhost:50051', - ServerCredentials.createSsl(null, [ - { - private_key: key, - cert_chain: cert, - }, - ]), - (err) => { - if (err) { - console.error(err) - done() - return - } + const port = await getPort() - const zbc = new ZeebeGrpcClient({ - config: { - CAMUNDA_OAUTH_DISABLED: true, - ZEEBE_ADDRESS: 'localhost:50051', - CAMUNDA_CUSTOM_ROOT_CERT_PATH: path.join(__dirname, 'localhost.crt'), - CAMUNDA_SECURE_CONNECTION: true, - zeebeGrpcSettings: { - ZEEBE_CLIENT_LOG_LEVEL: 'NONE', - }, + // Start the server + server.bindAsync( + `localhost:${port}`, + ServerCredentials.createSsl(null, [ + { + private_key: key, + cert_chain: cert, }, - }) - zbc.topology().then(() => { - expect(true).toBe(true) - zbc.close() - // Stop the server after the test - server.tryShutdown((err) => { - if (err) console.error(err) - done() + ]), + (err) => { + if (err) { + console.error(err) + resolve() + return + } + + const zbc = new ZeebeGrpcClient({ + config: { + CAMUNDA_OAUTH_DISABLED: true, + ZEEBE_ADDRESS: `localhost:${port}`, + CAMUNDA_CUSTOM_ROOT_CERT_PATH: path.join( + __dirname, + 'localhost.crt' + ), + CAMUNDA_SECURE_CONNECTION: true, + zeebeGrpcSettings: { + ZEEBE_CLIENT_LOG_LEVEL: 'NONE', + }, + }, }) - }) - } - ) + zbc.topology().then(() => { + expect(true).toBe(true) + zbc.close() + // Stop the server after the test + server.tryShutdown((err) => { + if (err) console.error(err) + resolve() + return + }) + }) + } + ) + }) }) test('gRPC server with self-signed certificate provided via string', (done) => { diff --git a/src/c8/lib/CamundaRestClient.ts b/src/c8/lib/CamundaRestClient.ts index 438814d7..56d6f4d8 100644 --- a/src/c8/lib/CamundaRestClient.ts +++ b/src/c8/lib/CamundaRestClient.ts @@ -76,7 +76,7 @@ class DefaultLosslessDto extends LosslessDto {} * `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 - * @experimental this API may be removed from this package in a future version, and moved to an ESM package. Can you use ESM in your project? Comment [on this issue](https://github.com/camunda/camunda-8-js-sdk/issues/267). + * @experimental this API may be moved to an ESM package in a future release. Can you use ESM in your project? Comment [on this issue](https://github.com/camunda/camunda-8-js-sdk/issues/267). */ export class CamundaRestClient { private userAgentString: string From 29e636bcfd451be99e1727017683b112fb96dc03 Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Wed, 23 Oct 2024 19:40:34 -0500 Subject: [PATCH 32/34] chore: update local test to 8.6.3 --- docker/.env | 2 +- package-lock.json | 9 +++++---- package.json | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docker/.env b/docker/.env index 66cf4d53..f709eba4 100644 --- a/docker/.env +++ b/docker/.env @@ -3,7 +3,7 @@ CAMUNDA_CONNECTORS_VERSION=8.5.0 CAMUNDA_OPTIMIZE_VERSION=8.5.0 CAMUNDA_PLATFORM_VERSION=8.6.0 -CAMUNDA_ZEEBE_VERSION=SNAPSHOT +CAMUNDA_ZEEBE_VERSION=8.6.3 CAMUNDA_WEB_MODELER_VERSION=8.5.0 ELASTIC_VERSION=8.9.0 KEYCLOAK_SERVER_VERSION=22.0.3 diff --git a/package-lock.json b/package-lock.json index c2f33c02..b1e79e7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "dayjs": "^1.8.15", "debug": "^4.3.4", "fast-xml-parser": "^4.1.3", - "form-data": "^4.0.0", + "form-data": "^4.0.1", "got": "^11.8.6", "jwt-decode": "^4.0.0", "lodash.mergewith": "^4.6.2", @@ -7959,9 +7959,10 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", diff --git a/package.json b/package.json index de3ac5ab..eea7ee97 100644 --- a/package.json +++ b/package.json @@ -150,7 +150,7 @@ "dayjs": "^1.8.15", "debug": "^4.3.4", "fast-xml-parser": "^4.1.3", - "form-data": "^4.0.0", + "form-data": "^4.0.1", "got": "^11.8.6", "jwt-decode": "^4.0.0", "lodash.mergewith": "^4.6.2", @@ -164,4 +164,4 @@ "uuid": "^7.0.3", "winston": "^3.14.2" } -} +} \ No newline at end of file From 60c246908f1f2c359ab7f3e69806754c4b5411d2 Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Wed, 23 Oct 2024 21:16:08 -0500 Subject: [PATCH 33/34] test: emit debug in migration rest test --- src/__tests__/c8/rest/migrateProcess.rest.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/__tests__/c8/rest/migrateProcess.rest.spec.ts b/src/__tests__/c8/rest/migrateProcess.rest.spec.ts index 95642609..420ae105 100644 --- a/src/__tests__/c8/rest/migrateProcess.rest.spec.ts +++ b/src/__tests__/c8/rest/migrateProcess.rest.spec.ts @@ -41,6 +41,9 @@ test('RestClient can migrate a process instance', async () => { worker: 'Migrant Worker 1', customHeadersDto: CustomHeaders, jobHandler: async (job) => { + // tslint:disable-next-line: no-console + console.log('job', job) // @DEBUG + instanceKey = job.processInstanceKey processVersion = job.customHeaders.ProcessVersion as number return job.complete().then(async (outcome) => { From fc5d5acf80be0724fd38906bb36e0fdbb6bd9556 Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Wed, 23 Oct 2024 21:27:50 -0500 Subject: [PATCH 34/34] test: correct version of Zeebe for local integration --- docker/docker-compose.yaml | 2 +- src/__tests__/c8/rest/migrateProcess.rest.spec.ts | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 9f93b71f..22237010 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -11,7 +11,7 @@ services: zeebe: # https://docs.camunda.io/docs/self-managed/platform-deployment/docker/#zeebe - image: camunda/zeebe:${CAMUNDA_PLATFORM_VERSION} + image: camunda/zeebe:${CAMUNDA_ZEEBE_VERSION} container_name: zeebe ports: - "26500:26500" diff --git a/src/__tests__/c8/rest/migrateProcess.rest.spec.ts b/src/__tests__/c8/rest/migrateProcess.rest.spec.ts index 420ae105..7acac7cd 100644 --- a/src/__tests__/c8/rest/migrateProcess.rest.spec.ts +++ b/src/__tests__/c8/rest/migrateProcess.rest.spec.ts @@ -1,7 +1,6 @@ import path from 'path' -import { CamundaJobWorker } from 'c8/lib/CamundaJobWorker' - +import { CamundaJobWorker } from '../../../c8/lib/CamundaJobWorker' import { CamundaRestClient } from '../../../c8/lib/CamundaRestClient' import { LosslessDto } from '../../../lib' @@ -41,9 +40,6 @@ test('RestClient can migrate a process instance', async () => { worker: 'Migrant Worker 1', customHeadersDto: CustomHeaders, jobHandler: async (job) => { - // tslint:disable-next-line: no-console - console.log('job', job) // @DEBUG - instanceKey = job.processInstanceKey processVersion = job.customHeaders.ProcessVersion as number return job.complete().then(async (outcome) => {