diff --git a/src/lib/diagram/mermaidState.ts b/src/lib/diagram/mermaidState.ts index 65d91e5a..03c12629 100644 --- a/src/lib/diagram/mermaidState.ts +++ b/src/lib/diagram/mermaidState.ts @@ -23,13 +23,22 @@ export class MermaidState { type?: string; transition?: string | Specification.Transition; end?: boolean | Specification.End; + compensatedBy?: string; onErrors?: Specification.Error[]; + usedForCompensation?: boolean; }, private isFirstState: boolean = false ) {} sourceCode() { - return this.definitions() + '\n' + this.transitions(); + const stateDefinition = this.definitions(); + const stateTransitions = this.transitions(); + + const stateDescription = stateTransitions.reduce((p, c) => { + return p + '\n' + c; + }, stateDefinition); + + return stateDescription; } private definitions(): string { @@ -41,7 +50,7 @@ export class MermaidState { ); } - private transitions(): string { + private transitions(): string[] { const transitions: string[] = []; transitions.push(...this.startTransition()); @@ -49,11 +58,10 @@ export class MermaidState { transitions.push(...this.eventConditionsTransition()); transitions.push(...this.errorTransitions()); transitions.push(...this.naturalTransition(this.stateKeyDiagram(this.state.name), this.state.transition)); + transitions.push(...this.compensatedByTransition()); transitions.push(...this.endTransition()); - return transitions.reduce((p, c) => { - return p + '\n' + c; - }); + return transitions; } private stateKeyDiagram(name: string | undefined) { @@ -178,34 +186,61 @@ export class MermaidState { return transitions; } + private compensatedByTransition() { + const transitions: string[] = []; + + if (this.state.compensatedBy) { + transitions.push(...this.naturalTransition(this.state.name, this.state.compensatedBy, 'compensated by')); + } + return transitions; + } + private definitionDetails() { + let definition: string | undefined; + switch (this.state.type) { case 'sleep': - return this.sleepStateDetails(); + definition = this.sleepStateDetails(); + break; case 'event': - return undefined; //NOTHING + // NOTHING + break; case 'operation': - return this.operationStateDetails(); + definition = this.operationStateDetails(); + break; case 'parallel': - return this.parallelStateDetails(); + definition = this.parallelStateDetails(); + break; case 'switch': const switchState: any = this.state; if (switchState.dataConditions) { - return this.dataBasedSwitchStateDetails(); + definition = this.dataBasedSwitchStateDetails(); + break; } if (switchState.eventConditions) { - return this.eventBasedSwitchStateDetails(); + definition = this.eventBasedSwitchStateDetails(); + break; } throw new Error(`Unexpected switch type; \n state value= ${JSON.stringify(this.state, null, 4)}`); case 'inject': - return undefined; // NOTHING + // NOTHING + break; case 'foreach': - return this.foreachStateDetails(); + definition = this.foreachStateDetails(); + break; case 'callback': - return this.callbackStateDetails(); + definition = this.callbackStateDetails(); + break; default: throw new Error(`Unexpected type= ${this.state.type}; \n state value= ${JSON.stringify(this.state, null, 4)}`); } + + if (this.state.usedForCompensation) { + definition = definition ? definition : ''; + definition = this.stateDescription(this.stateKeyDiagram(this.state.name), 'usedForCompensation\n') + definition; + } + + return definition ? definition : undefined; } private definitionType() { @@ -349,7 +384,7 @@ export class MermaidState { return this.stateKeyDiagram(source) + ' --> ' + this.stateKeyDiagram(target) + (label ? ' : ' + label : ''); } - private stateDescription(stateName: string | undefined, description: string, value: string) { - return stateName + ` : ${description} = ${value}`; + private stateDescription(stateName: string | undefined, description: string, value?: string) { + return stateName + ` : ${description}${value !== undefined ? ' = ' + value : ''}`; } } diff --git a/tests/lib/diagram/mermaidDiagram.spec.ts b/tests/lib/diagram/mermaidDiagram.spec.ts index ab60430a..6f18f32f 100644 --- a/tests/lib/diagram/mermaidDiagram.spec.ts +++ b/tests/lib/diagram/mermaidDiagram.spec.ts @@ -19,8 +19,8 @@ import fs from 'fs'; describe('MermaidDiagram', () => { it('should create mermaid diagram source code', () => { - const expected = fs.readFileSync('./tests/examples/jobmonitoring.json', 'utf8'); - const actual = new MermaidDiagram(Specification.Workflow.fromSource(expected)).sourceCode(); + const jsonSource = fs.readFileSync('./tests/examples/jobmonitoring.json', 'utf8'); + const actual = new MermaidDiagram(Specification.Workflow.fromSource(jsonSource)).sourceCode(); expect(actual).toBe(`stateDiagram-v2 SubmitJob : SubmitJob SubmitJob : type = Operation State @@ -59,4 +59,28 @@ JobFailed : Action mode = sequential JobFailed : Num. of actions = 1 JobFailed --> [*]`); }); + + it(`should handle compensated by`, () => { + const jsonSource = fs.readFileSync('./tests/lib/diagram/wf_with_compensation.json', 'utf8'); + const actual = new MermaidDiagram(Specification.Workflow.fromSource(jsonSource)).sourceCode(); + + expect(actual).toBe(`stateDiagram-v2 +Item_Purchase : Item Purchase +Item_Purchase : type = Event State +[*] --> Item_Purchase +Item_Purchase --> Cancel_Purchase : compensated by +Item_Purchase --> [*] + +Cancel_Purchase : Cancel Purchase +Cancel_Purchase : type = Operation State +Cancel_Purchase : usedForCompensation +Cancel_Purchase : Action mode = sequential +Cancel_Purchase : Num. of actions = 1 +Cancel_Purchase --> Send_confirmation_purchase_cancelled + +Send_confirmation_purchase_cancelled : Send confirmation purchase cancelled +Send_confirmation_purchase_cancelled : type = Operation State +Send_confirmation_purchase_cancelled : Action mode = sequential +Send_confirmation_purchase_cancelled : Num. of actions = 1`); + }); }); diff --git a/tests/lib/diagram/wf_with_compensation.json b/tests/lib/diagram/wf_with_compensation.json new file mode 100644 index 00000000..627fcf4b --- /dev/null +++ b/tests/lib/diagram/wf_with_compensation.json @@ -0,0 +1,74 @@ +{ + "id": "newItemPurchaseWorkflow", + "version": "1.0", + "specVersion": "0.8", + "name": "New Item Purchase Workflow", + "states": [ + { + "name": "Item Purchase", + "type": "event", + "onEvents": [ + { + "eventRefs": [ + "New Purchase Event" + ], + "actions": [ + { + "functionRef": { + "refName": "Invoke Debit Customer Function", + "arguments": { + "customerid": "${ .purchase.customerid }", + "amount": "${ .purchase.amount }" + } + } + } + ] + } + ], + "compensatedBy": "Cancel Purchase", + "end": true, + "onErrors": [ + { + "errorRef": "Debit Error", + "end": { + "compensate": true + } + } + ] + }, + { + "name": "Cancel Purchase", + "type": "operation", + "usedForCompensation": true, + "actions": [ + { + "functionRef": { + "refName": "Invoke Credit Customer Function", + "arguments": { + "customerid": "${ .purchase.customerid }", + "amount": "${ .purchase.amount }" + } + } + } + ], + "transition": "Send confirmation purchase cancelled" + }, + { + "name": "Send confirmation purchase cancelled", + "type": "operation", + "actions": [ + { + "functionRef": { + "refName": "Send email", + "arguments": { + "customerid": "${ .purchase.customerid }", + } + } + } + ] + } + ], + "functions": "http://myservicedefs.io/graphqldef.json", + "events": "http://myeventdefs.io/eventdefs.json", + "errors": "file://mydefs/errordefs.json" +} \ No newline at end of file