Workflow - ksenianishchenko/trivia-app GitHub Wiki

Overview

The Workflow is a separate module whose function is to control a wizard flow. The workflow definition is similar to Amazon State Language for AWS Step Functions and describes the entire flow and branching.

Workflow Definition

There is a similar workflow definition described by AWS - Step Functions. To fully understand the idea - read here for more details https://docs.aws.amazon.com/step-functions/latest/dg/concepts-amazon-states-language.html.

GET /api/v1/trivia/{trivia_id}
200 OK application/json
{
  "startAt": "0",
  "steps": {
    "0": {
      "type": "TriviaQuestion",
      "id": "0",
      "next": "1",
      "end": false
    },
    "1": {
      "type": "TriviaQuestion",
      "end": false,
      "next": "display-results-step"
    },
    "display-results-step": {
      "type": "TriviaResult",
      "end": true
    }
  }
}

startAt - indicates the first step of the workflow

steps: describes all steps and how they interact with each other

Workflow Router

In the example above, the workflow definition provides a list of all steps within a trivia and describes the navigation behavior between steps. We must now understand how to read it and use it to display or navigate to a certain step. We will introduce the IWorkflowRouter interface, and all its implementations will deal with the exact Workflow Step which it can handle. First, we will need a WorkflowStep type with the single property "type":

export default interface WorkflowStep {
  type: string;
};

The type property is the most important one, and it will identify the workflow step type - whether it's a TriviaQuestion, TriviaResult, FeedbackQuestion, etc.

The Trivia Question workflow step will extend the WorkflowStep interface with specific fields for the Trivia Questions:

export default class TriviaQuestionWorkflowStep implements WorkflowStep {
  type: string;
  end: boolean;
  next: string | undefined;
  questionId: string;
}

Where:

questionId - the Trivia Question ID

end - indicates whether this question is the last one, and once a user submits an answer, the workflow terminates the trivia by redirecting a user to the List Trivia page

next - the next Step ID within the Workflow Definition. Once a user submits an answer to the current question, the "next" property will force the Workflow Controller to process the next step within the workflow.

Workflow Router Implementation

We are using the IWorkflowRouter interface, which describes the common API methods for the concrete implementations :

export default interface IWorkflowRouter {
  route(step: WorkflowStep): void;
}

For the Workflow Step with the type property value of "TriviaQuestion" the corresponding Workflow Router will be:

export default class TriviaQuestionWorkflowRouter implements IWorkflowRouter {
  route(step: WorkflowStep): void {
    if (step.type !== "TriviaQuestion") {
      return;
    }

    const triviaQuestionStep = step as TriviaQuestionWorkflowStep;
    console.log($`Routing to Trivia Question Component ${triviaQuestionStep.questionId}`);
    // here goes the URL navigation redirect to the Trivia Question Component
  }
}
export default class TriviaQuestionResultWorkflowRouter implements IWorkflowRouter {
  route(step: WorkflowStep): void {
    if (step.type !== "TriviaResults") {
      return;
    }

    const triviaResultStep = step as TriviaResultWorkflowStep;
    console.log(`Navigating to Trivia Questions Result component`);
  }
}

Workflow Controller

We can't just use these IWorkflowRouter implementations alone. Somehow, we must store the current workflow definition, understand where we are currently, and do specific actions. For example, when to redirect a user to the question or when to show the results. This is something that must be used in the same way as the Redux state.

export default class WorkflowController {
  private readonly _routers: Map<string, IWorkflowRouter>;
  private _workflowDefinition: WorkflowDefinition;
  private _currentStepId: string;

  constructor() {
    this._routers = new Map<string, IWorkflowRouter>();
  }

  addRouter(workflowRouter: IWorkflowRouter) {
    this._routers.set(workflowRouter.getType(), workflowRouter);
  }

  initialize(definition: WorkflowDefinition) {
    this._workflowDefinition = definition;
    this._currentStepId = definition.startAt;
  }

  routeToCurrent() {
    if (!this._workflowDefinition) {
      throw Error("No workflow definition was initialized");
    }

    const currentStep = this._workflowDefinition.steps.get(this._currentStepId);
    if (!currentStep) {
      return;
    }

    const router = this._routers.get(currentStep.type);
    if (router) {
      router.route(currentStep);
    }
  }
  
  afterSubmit() {
    const currentStep = this._workflowDefinition.steps.get(this._currentStepId);
    if (currentStep) {
      if (currentStep.next) {
        this._currentStepId = currentStep.next;
        this.routeToCurrent();
      }
      else if (currentStep.end) {
        // Navigate to the home page for now
      }
    }
  };
}