Implementing a Clean Architecture Modular Application in Nuxt/Vue Typescript — Part 3: Vuex Store

Introduction

Interactor class

Unit test for the interactor class

import { cid, container, mockTransient, resetContainer } from "inversify-props"
import { containerBuilder } from "@@/src/ui/plugins/inversify"
import { ParseError } from "@@/src/app/shared/error/ParseError"
import { HttpError } from "@@/src/app/shared/http"
import { TestActions } from "@@/src/app/shared/http/HttpService.mock"
import { ITaskRepository } from "../../domain"
import { mockTaskData } from "../../domain/__tests__/Task.mock"
import { MockTaskService } from "../../infrastructure/__tests__/TaskService.mock"
import { GetMany } from "../GetMany"
beforeEach(() => {
resetContainer()
containerBuilder()
mockTransient<ITaskRepository>(cid.TaskService, MockTaskService)
})
describe('GetMany', () => { it('should call success callback', async () => {
container.bind<TestActions>('ActionType').toConstantValue('ok')
const getMany = new GetMany()
const mockCallbacks = {
respondWithSuccess: jest.fn(),
respondWithClientError: jest.fn(),
respondWithParseError: jest.fn(),
respondWithServerError: jest.fn()
}
await getMany.execute(null, mockCallbacks)
expect(mockCallbacks.respondWithSuccess).toHaveBeenCalledWith(mockTaskData())
})
it('should call client error callback', async () => {
container.bind<TestActions>('ActionType').toConstantValue('clientError')
const getMany = new GetMany()
const mockCallbacks = {
respondWithSuccess: jest.fn(),
respondWithClientError: jest.fn(),
respondWithParseError: jest.fn(),
respondWithServerError: jest.fn()
}
await getMany.execute(null, mockCallbacks)
expect(mockCallbacks.respondWithClientError.mock.calls[0][0]).toBeInstanceOf(HttpError)
})
it('should call server error callback', async () => {
container.bind<TestActions>('ActionType').toConstantValue('serverError')
const getMany = new GetMany()
const mockCallbacks = {
respondWithSuccess: jest.fn(),
respondWithClientError: jest.fn(),
respondWithParseError: jest.fn(),
respondWithServerError: jest.fn()
}
await getMany.execute(null, mockCallbacks)
expect(mockCallbacks.respondWithServerError.mock.calls[0][0]).toBeInstanceOf(HttpError)
})
it('should call parse error callback', async () => {
container.bind<TestActions>('ActionType').toConstantValue('parseError')
const getMany = new GetMany()
const mockCallbacks = {
respondWithSuccess: jest.fn(),
respondWithClientError: jest.fn(),
respondWithParseError: jest.fn(),
respondWithServerError: jest.fn()
}
await getMany.execute(null, mockCallbacks)
expect(mockCallbacks.respondWithParseError.mock.calls[0][0]).toBeInstanceOf(ParseError)
})
})
import { ParseError } from "@@/src/app/shared/error/ParseError";
import { HttpError } from "@@/src/app/shared/http";
import { TestActions } from "@@/src/app/shared/http/HttpService.mock";
import { inject } from "inversify-props";
import { err, ok, Result } from "neverthrow";
import { ITaskData, ITaskRepository } from "../../domain";
import { mockTaskData } from "../../domain/__tests__/Task.mock";
export class MockTaskService implements ITaskRepository {
actionType: TestActions
constructor(@inject('ActionType') actionType: TestActions) {
this.actionType = actionType
}
create(): Promise<Result<ITaskData, ParseError | HttpError>> {
throw new Error("Method not implemented.");
}
remove(): Promise<Result<ITaskData, ParseError | HttpError>> {
throw new Error("Method not implemented.");
}
edit(): Promise<Result<ITaskData, ParseError | HttpError>> {
throw new Error("Method not implemented.");
}
getMany(): Promise<Result<ITaskData[], ParseError | HttpError>> {
if (this.actionType === 'ok') {
return new Promise((resolve) => {
resolve(ok(mockTaskData()))
})
}
else if (this.actionType === 'clientError') {
return new Promise((reject) => {
reject(err(new HttpError(404)))
})
}
else if (this.actionType === 'serverError') {
return new Promise((reject) => {
reject(err(new HttpError(500)))
})
}
else {
return new Promise((reject) => {
reject(err(new ParseError('Error parsing data')))
})
}
}
getOne(): Promise<Result<ITaskData, ParseError | HttpError>> {
throw new Error("Method not implemented.");
}
}

The code for the interactor class

import { ParseError } from '@@/src/app/shared/error/ParseError'
import { HttpError, isHttpError } from '@@/src/app/shared/http'
import { inject } from 'inversify-props'
import { ITaskData, ITaskRepository } from '../domain'
export type Callbacks = {
respondWithSuccess(data: ITaskData[]): void
respondWithClientError(error: HttpError): void
respondWithServerError(error: HttpError): void
respondWithParseError(error: ParseError): void
}
export interface IUseCase {
execute(params: any, callbacks: Callbacks): void
}
export class GetMany implements IUseCase {
@inject() taskService!: ITaskRepository
async execute(
_params: any,
{
respondWithSuccess,
respondWithClientError,
respondWithServerError,
respondWithParseError,
}: Callbacks
): Promise<void> {
const result = await this.taskService.getMany()
result.map((tasks) => {
respondWithSuccess(tasks)
}).mapErr((error) => {
if (isHttpError(error)) {
if (error.isClientError()) {
respondWithClientError(error)
return
}
respondWithServerError(error)
}
respondWithParseError(error)
})
}
}

Vuex Store

Store creator and Storage

import Vuex, { Store } from 'vuex'
import { inject } from 'inversify-props'
import { getModule } from 'nuxt-property-decorator'
import { ITaskState, TaskStore } from '../../modules/task/storage'
export interface IRootState {
tasks: ITaskState
}
export interface IStoreCreator {
makeStore(): Store<IRootState>
}
export class StoreCreator implements IStoreCreator {
private store: Store<IRootState> | null = null
makeStore(): Store<IRootState> {
if (!this.store) {
this.store = new Vuex.Store<IRootState>({
modules: {
tasks: TaskStore,
},
})
}
return this.store
}
}
export interface IStorage {
getStores(): { taskStore: TaskStore }
}
export class Storage implements IStorage {
private taskStore: TaskStore
constructor(@inject() storeCreator: IStoreCreator) {
const store = storeCreator.makeStore()
this.taskStore = getModule(TaskStore, store)
}
getStores() {
return { taskStore: this.taskStore }
}
}
import 'reflect-metadata'
import { cid, container } from 'inversify-props'
import { AxiosCreator, HttpService, IAxiosCreator, IHttpService } from '@@/src/app/shared/http'
import { ITaskRepository } from '@@/src/app/modules/task/domain'
import { TaskService } from '@@/src/app/modules/task/infrastructure'
import { GetMany, IUseCase } from '@@/src/app/modules/task/usecase/GetMany'
import { IStorage, IStoreCreator, StoreCreator, Storage } from '@@/src/app/shared/storage/Storage'
export function containerBuilder() {
container.addTransient<IAxiosCreator>(AxiosCreator)
container.addTransient<IHttpService>(HttpService)
container.addTransient<ITaskRepository>(TaskService)
container.addTransient<IUseCase>(GetMany)
container.addSingleton<IStoreCreator>(StoreCreator)
container.addSingleton<IStorage>(Storage)
}
containerBuilder()

TaskStore

import { createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
const Vue = createLocalVue()
Vue.use(Vuex)
import { cid, container, mockTransient, resetContainer } from "inversify-props"
import { createLocalVue } from '@vue/test-utils'
import { containerBuilder } from "@@/src/ui/plugins/inversify"
import { TestActions } from "@@/src/app/shared/http/HttpService.mock"
import { IStorage } from "@@/src/app/shared/storage/Storage"
import { HttpError } from "@@/src/app/shared/http"
import { ParseError } from "@@/src/app/shared/error/ParseError"
import Vuex from 'vuex'
import { ITaskRepository } from "../../domain"
import { MockTaskService } from "../../infrastructure/__tests__/TaskService.mock"
import { mockTaskData } from "../../domain/__tests__/Task.mock"
const Vue = createLocalVue()
Vue.use(Vuex)
beforeEach(() => {
resetContainer()
containerBuilder()
mockTransient<ITaskRepository>(cid.TaskService, MockTaskService)
})
describe('TaskStore', () => {
describe('Actions', () => {
describe('fetchTasks', () => {
it('should respond with success', async () => {
container.bind<TestActions>('ActionType').toConstantValue('ok')
const storage = container.get<IStorage>(cid.Storage)
const store = storage.getStores().taskStore
await store.fetchTasks()
expect(store.taskList).toStrictEqual(mockTaskData())
expect(store.error).toBeNull()
expect(store.loading).toBeFalsy()
})
it('should respond with client error', async () => {
container.bind<TestActions>('ActionType').toConstantValue('clientError')
const storage = container.get<IStorage>(cid.Storage)
const store = storage.getStores().taskStore
await store.fetchTasks()
expect(store.taskList).toStrictEqual([])
expect(store.error).toBeInstanceOf(HttpError)
expect(store.loading).toBeFalsy()
})
it('should respond with server error', async () => {
container.bind<TestActions>('ActionType').toConstantValue('serverError')
const storage = container.get<IStorage>(cid.Storage)
const store = storage.getStores().taskStore
await store.fetchTasks()
expect(store.taskList).toStrictEqual([])
expect(store.error).toBeInstanceOf(HttpError)
expect(store.loading).toBeFalsy()
})
it('should respond with parse error', async () => {
container.bind<TestActions>('ActionType').toConstantValue('parseError')
const storage = container.get<IStorage>(cid.Storage)
const store = storage.getStores().taskStore
await store.fetchTasks()
expect(store.taskList).toStrictEqual([])
expect(store.error).toBeInstanceOf(ParseError)
expect(store.loading).toBeFalsy()
})
})
})
})

TaskStore code

import { inject } from 'inversify-props'
import { Module, VuexModule, VuexMutation, VuexAction } from 'nuxt-property-decorator'
import { ITaskData } from '../domain'
import { GetMany } from '../usecase/GetMany'
export interface ITaskState {
taskList: ITaskData[]
error: Error | null
loading: boolean
}
@Module({ namespaced: true, name: 'tasks', stateFactory: true })
export class TaskStore extends VuexModule implements ITaskState {
@inject() getMany!: GetMany
taskList: ITaskData[] = []
error: Error | null = null
loading: boolean = false
@VuexMutation
setTaskList(tList: ITaskData[]) {
this.taskList = tList
}
@VuexMutation
setError(error: Error | null) {
this.error = error
}
@VuexMutation
setLoading(loading: boolean) {
this.loading = loading
}
@VuexAction({ rawError: true })
async fetchTasks() {
this.setLoading(true)
this.getMany.execute(null, {
respondWithSuccess: (data: ITaskData[]) => {
this.setTaskList(data)
this.setError(null)
this.setLoading(false)
},
respondWithClientError: (error: Error) => {
this.setError(error)
this.setTaskList([])
this.setLoading(false)
},
respondWithServerError: (error: Error) => {
this.setError(error)
this.setTaskList([])
this.setLoading(false)
},
respondWithParseError: (error: Error) => {
this.setError(error)
this.setTaskList([])
this.setLoading(false)
}
})
}
}

Vuex plugin

import { cid, container } from "inversify-props"
import Vuex, { Store } from 'vuex'
import Vue from 'vue'
import { StoreCreator } from "@@/src/app/shared/storage/Storage"
Vue.use(Vuex)
const storeCreator = container.get<StoreCreator>(cid.StoreCreator)
declare module 'vue/types/vue' {
interface Vue {
$store: Store<any>
}
}
Vue.prototype.$store = storeCreator.makeStore()

Conclusion