Skip to content

Handle compensatedBy in diagram generation #161

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 51 additions & 16 deletions src/lib/diagram/mermaidState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -41,19 +50,18 @@ export class MermaidState {
);
}

private transitions(): string {
private transitions(): string[] {
const transitions: string[] = [];

transitions.push(...this.startTransition());
transitions.push(...this.dataConditionsTransitions());
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) {
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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 : ''}`;
}
}
28 changes: 26 additions & 2 deletions tests/lib/diagram/mermaidDiagram.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`);
});
});
74 changes: 74 additions & 0 deletions tests/lib/diagram/wf_with_compensation.json
Original file line number Diff line number Diff line change
@@ -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"
}