Skip to content

Commit

Permalink
feat(camunda8): implement createProcessInstanceWithResult
Browse files Browse the repository at this point in the history
  • Loading branch information
jwulf committed Sep 27, 2024
1 parent ff248b7 commit 4ec4fa1
Show file tree
Hide file tree
Showing 5 changed files with 311 additions and 6 deletions.
90 changes: 90 additions & 0 deletions src/__tests__/c8/rest/createProcessRest.spec.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
32 changes: 32 additions & 0 deletions src/__tests__/testdata/create-process-rest.bpmn
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_1ijuty4" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.27.0" modeler:executionPlatform="Camunda Cloud" modeler:executionPlatformVersion="8.5.0">
<bpmn:process id="create-process-rest" name="Create Process Test" isExecutable="true">
<bpmn:startEvent id="StartEvent_1" name="Start">
<bpmn:outgoing>Flow_15yxzfg</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:endEvent id="Event_1j84ets" name="End">
<bpmn:incoming>Flow_15yxzfg</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_15yxzfg" sourceRef="StartEvent_1" targetRef="Event_1j84ets" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="create-process-rest">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="79" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="185" y="122" width="24" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1j84ets_di" bpmnElement="Event_1j84ets">
<dc:Bounds x="292" y="79" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="300" y="122" width="20" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_15yxzfg_di" bpmnElement="Flow_15yxzfg">
<di:waypoint x="215" y="97" />
<di:waypoint x="292" y="97" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
70 changes: 69 additions & 1 deletion src/c8/lib/C8Dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export class DeployResourceResponse extends DeployResourceResponseDto {
forms!: FormDeployment[]
}

export class CreateProcessInstanceResponse extends LosslessDto {
export class CreateProcessInstanceResponse<T = Record<string, never>> {
/**
* The unique key identifying the process definition (e.g. returned from a process
* in the DeployResourceResponse message)
Expand All @@ -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 {
Expand Down Expand Up @@ -212,3 +216,67 @@ export class PublishMessageResponse extends LosslessDto {
/** the tenantId of the message */
tenantId!: string
}

export interface CreateProcessBaseRequest<V extends JSONDoc | LosslessDto> {
/**
* 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<V> {
/**
* the BPMN process ID of the process definition
*/
bpmnProcessId: string
}

export interface CreateProcessInstanceFromProcessDefinition<
V extends JSONDoc | LosslessDto,
> extends CreateProcessBaseRequest<V> {
/**
* the key of the process definition
*/
processDefinitionKey: string
}

export type CreateProcessInstanceReq<T extends JSONDoc | LosslessDto> =
| CreateProcessInstanceFromBpmnProcessId<T>
| CreateProcessInstanceFromProcessDefinition<T>
70 changes: 65 additions & 5 deletions src/c8/lib/C8RestClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import {
ActivateJobsRequest,
BroadcastSignalReq,
CompleteJobRequest,
CreateProcessInstanceReq,
ErrorJobWithVariables,
FailJobRequest,
IProcessVariables,
Expand All @@ -41,6 +40,7 @@ import {
import {
BroadcastSignalResponse,
CorrelateMessageResponse,
CreateProcessInstanceReq,
CreateProcessInstanceResponse,
Ctor,
DecisionDeployment,
Expand All @@ -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
Expand Down Expand Up @@ -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<T extends JSONDoc>(
public async createProcessInstance<T extends JSONDoc | LosslessDto>(
request: CreateProcessInstanceReq<T>
): Promise<CreateProcessInstanceResponse<Record<string, never>>>

async createProcessInstance<
T extends JSONDoc | LosslessDto,
V extends LosslessDto,
>(
request: CreateProcessInstanceReq<T> & {
outputVariablesDto?: Ctor<V>
}
) {
const headers = await this.getHeaders()

const outputVariablesDto: Ctor<V> | Ctor<LosslessDto> =
(request.outputVariablesDto as Ctor<V>) ?? 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<CreateProcessInstanceResponse>()
.json<
InstanceType<typeof CreateProcessInstanceResponseWithVariablesDto>
>()
)
}

/**
* Create and start a process instance. This method awaits the outcome of the process.
*/
public async createProcessInstanceWithResult<T extends JSONDoc | LosslessDto>(
request: CreateProcessInstanceReq<T>
): Promise<CreateProcessInstanceResponse<unknown>>

public async createProcessInstanceWithResult<
T extends JSONDoc | LosslessDto,
V extends LosslessDto,
>(
request: CreateProcessInstanceReq<T> & {
outputVariablesDto: Ctor<V>
}
): Promise<CreateProcessInstanceResponse<V>>
public async createProcessInstanceWithResult<
T extends JSONDoc | LosslessDto,
V,
>(
request: CreateProcessInstanceReq<T> & {
outputVariablesDto?: Ctor<V>
}
) {
/**
* 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<T>)
}

/**
* Cancel an active process instance
*/
Expand Down
55 changes: 55 additions & 0 deletions src/c8/lib/RestApiProcessInstanceClassFactory.ts
Original file line number Diff line number Diff line change
@@ -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<string, any>()

return function <Variables extends LosslessDto>(
outputVariableDto: new (obj: any) => Variables
): new (obj: any) => CreateProcessInstanceResponse<Variables> {
// 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!: 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
}
}

0 comments on commit 4ec4fa1

Please sign in to comment.