Simplify types for CRUDActions & CRUDReducer

This commit is contained in:
Jerko Steiner 2019-04-02 11:44:34 +08:00
parent 8b6f90235e
commit 580fb368e6
9 changed files with 395 additions and 321 deletions

View File

@ -1,6 +1,7 @@
import {createCRUDActions} from './CRUDActions'
import React from 'react' import React from 'react'
import {AnyAction} from 'redux' import {AnyAction} from 'redux'
import {CRUDActions, CRUDReducer, ICRUDMethod} from './' import {CRUDReducer, ICRUDMethod} from './'
import {HTTPClientMock, TestUtils, getError} from '../test-utils' import {HTTPClientMock, TestUtils, getError} from '../test-utils'
import {IMethod} from '@rondo/common' import {IMethod} from '@rondo/common'
import {IPendingAction} from '../actions' import {IPendingAction} from '../actions'
@ -55,13 +56,13 @@ describe('CRUD', () => {
} }
const http = new HTTPClientMock<ITestAPI>() const http = new HTTPClientMock<ITestAPI>()
const actions = CRUDActions.fromTwoRoutes({ const actions = createCRUDActions(
http, http,
listRoute: '/one/:oneId/two', '/one/:oneId/two',
specificRoute: '/one/:oneId/two/:twoId', '/one/:oneId/two/:twoId',
actionName: 'TEST', 'TEST',
}) )
const crudReducer = new CRUDReducer<ITwo>('TEST') const crudReducer = new CRUDReducer<ITwo, 'TEST'>('TEST')
const Crud = crudReducer.reduce const Crud = crudReducer.reduce
const test = new TestUtils() const test = new TestUtils()
@ -109,13 +110,23 @@ describe('CRUD', () => {
} }
function getUrl(method: ICRUDMethod) { function getUrl(method: ICRUDMethod) {
return method === 'post' || method === 'getMany' return method === 'save' || method === 'findMany'
? '/one/1/two' ? '/one/1/two'
: '/one/1/two/2' : '/one/1/two/2'
} }
function getHTTPMethod(method: ICRUDMethod): IMethod { function getHTTPMethod(method: ICRUDMethod): IMethod {
return method === 'getMany' ? 'get' : method switch (method) {
case 'save':
return 'post'
case 'update':
return 'put'
case 'remove':
return 'delete'
case 'findOne':
case 'findMany':
return 'get'
}
} }
describe('Promise rejections', () => { describe('Promise rejections', () => {
@ -123,29 +134,29 @@ describe('CRUD', () => {
method: ICRUDMethod method: ICRUDMethod
params: any params: any
}> = [{ }> = [{
method: 'get', method: 'findOne',
params: { params: {
params: {oneId: 1, twoId: 2}, params: {oneId: 1, twoId: 2},
}, },
}, { }, {
method: 'getMany', method: 'findMany',
params: { params: {
params: {oneId: 1}, params: {oneId: 1},
}, },
}, { }, {
method: 'post', method: 'save',
params: { params: {
body: {name: 'test'}, body: {name: 'test'},
params: {oneId: 1, twoId: 2}, params: {oneId: 1, twoId: 2},
}, },
}, { }, {
method: 'put', method: 'update',
params: { params: {
body: {name: 'test'}, body: {name: 'test'},
params: {oneId: 1, twoId: 2}, params: {oneId: 1, twoId: 2},
}, },
}, { }, {
method: 'delete', method: 'remove',
params: { params: {
body: {}, body: {},
params: {oneId: 1, twoId: 2}, params: {oneId: 1, twoId: 2},
@ -160,9 +171,10 @@ describe('CRUD', () => {
http.mockAdd({ http.mockAdd({
url: getUrl(method), url: getUrl(method),
method: getHTTPMethod(method), method: getHTTPMethod(method),
data: method === 'put' || method === 'post' || method === 'delete' data: method === 'save'
|| method === 'update' || method === 'remove'
? testCase.params.body ? testCase.params.body
: undefined, : undefined,
}, {error: 'Test Error'}, 400) }, {error: 'Test Error'}, 400)
}) })
@ -196,25 +208,25 @@ describe('CRUD', () => {
body?: any body?: any
response: any response: any
}> = [{ }> = [{
method: 'getMany', method: 'findMany',
params: {oneId: 1, twoId: 2}, params: {oneId: 1, twoId: 2},
response: [entity], response: [entity],
}, { }, {
method: 'get', method: 'findOne',
params: {oneId: 1, twoId: 2}, params: {oneId: 1, twoId: 2},
response: entity, response: entity,
}, { }, {
method: 'post', method: 'save',
params: {oneId: 1}, params: {oneId: 1},
body: {name: entity.name}, body: {name: entity.name},
response: entity, response: entity,
}, { }, {
method: 'put', method: 'update',
params: {oneId: 1, twoId: 2}, params: {oneId: 1, twoId: 2},
body: {name: entity.name}, body: {name: entity.name},
response: entity, response: entity,
}, { }, {
method: 'delete', method: 'remove',
params: {oneId: 1, twoId: 2}, params: {oneId: 1, twoId: 2},
response: {id: entity.id}, response: {id: entity.id},
}] }]
@ -243,12 +255,12 @@ describe('CRUD', () => {
})) }))
await action.payload await action.payload
const state = store.getState() const state = store.getState()
expect(state.Crud.status.getMany.isLoading).toBe(false) expect(state.Crud.status.findMany.isLoading).toBe(false)
if (method === 'delete') { if (method === 'remove') {
expect(state.Crud.ids).toEqual([]) expect(state.Crud.ids).toEqual([])
expect(state.Crud.byId[entity.id]).toBe(undefined) expect(state.Crud.byId[entity.id]).toBe(undefined)
} else { } else {
if (method !== 'put') { if (method !== 'update') {
expect(state.Crud.ids).toEqual([entity.id]) expect(state.Crud.ids).toEqual([entity.id])
} }
expect(state.Crud.byId[entity.id]).toEqual(entity) expect(state.Crud.byId[entity.id]).toEqual(entity)
@ -259,20 +271,20 @@ describe('CRUD', () => {
describe('POST then DELETE', () => { describe('POST then DELETE', () => {
const postTestCase = testCases.find(t => t.method === 'post')! const saveTestCase = testCases.find(t => t.method === 'save')!
const deleteTestCase = testCases.find(t => t.method === 'delete')! const removeTestCase = testCases.find(t => t.method === 'remove')!
beforeEach(() => { beforeEach(() => {
http.mockAdd({ http.mockAdd({
url: getUrl(postTestCase.method), url: getUrl(saveTestCase.method),
method: getHTTPMethod(postTestCase.method), method: getHTTPMethod(saveTestCase.method),
data: postTestCase.body, data: saveTestCase.body,
}, postTestCase.response) }, saveTestCase.response)
http.mockAdd({ http.mockAdd({
url: getUrl(deleteTestCase.method), url: getUrl(removeTestCase.method),
method: getHTTPMethod(deleteTestCase.method), method: getHTTPMethod(removeTestCase.method),
data: deleteTestCase.body, data: removeTestCase.body,
}, deleteTestCase.response) }, removeTestCase.response)
}) })
afterEach(() => { afterEach(() => {
@ -280,15 +292,15 @@ describe('CRUD', () => {
}) })
it('removes id and entity from state', async () => { it('removes id and entity from state', async () => {
const action1 = store.dispatch(actions.post({ const action1 = store.dispatch(actions.save({
params: postTestCase.params, params: saveTestCase.params,
body: postTestCase.body, body: saveTestCase.body,
})) }))
await action1.payload await action1.payload
expect(store.getState().Crud.ids).toEqual([entity.id]) expect(store.getState().Crud.ids).toEqual([entity.id])
const action2 = store.dispatch(actions.delete({ const action2 = store.dispatch(actions.remove({
params: deleteTestCase.params, params: removeTestCase.params,
body: deleteTestCase.body, body: removeTestCase.body,
})) }))
await action2.payload await action2.payload
expect(store.getState().Crud.ids).toEqual([]) expect(store.getState().Crud.ids).toEqual([])

View File

@ -1,119 +1,159 @@
import {IRoutes} from '@rondo/common' import {ICRUDAction} from './ICRUDAction'
import {ICRUDMethod} from './ICRUDMethod'
import {IHTTPClient, ITypedRequestParams} from '../http' import {IHTTPClient, ITypedRequestParams} from '../http'
import {IRoutes} from '@rondo/common'
export type Optional<T> = T extends {} ? T : undefined export type Optional<T> = T extends {} ? T : undefined
interface ICRUDActionTypes { type Filter<T, U> = T extends U ? T : never
readonly get: string
readonly put: string
readonly post: string
readonly delete: string
readonly getMany: string
}
export class CRUDActions< type Action<T, ActionType extends string, Method extends ICRUDMethod> =
Filter<ICRUDAction<T, ActionType>, {method: Method, status: 'pending'}>
export class SaveActionCreator<
T extends IRoutes, T extends IRoutes,
POST extends keyof T & string, Route extends keyof T & string,
GET_MANY extends keyof T & string, ActionType extends string,
GET extends keyof T & string,
PUT extends keyof T & string,
DELETE extends keyof T & string,
> { > {
readonly actionTypes: ICRUDActionTypes
constructor( constructor(
readonly http: IHTTPClient<T>, readonly http: IHTTPClient<T>,
readonly postRoute: POST, readonly route: Route,
readonly getManyRoute: GET_MANY, readonly type: ActionType,
readonly getRoute: GET, ) {}
readonly putRoute: PUT,
readonly deleteRoute: DELETE,
readonly actionName: string,
) {
this.actionTypes = this.getActionTypes()
}
static fromTwoRoutes< save = (params: {
R, body: T[Route]['post']['body'],
S extends keyof R & string, params: T[Route]['post']['params'],
L extends keyof R & string, }): Action<T[Route]['post']['response'], ActionType, 'save'> => {
>(params: {
http: IHTTPClient<R>,
specificRoute: S,
listRoute: L,
actionName: string,
},
) {
const {http, specificRoute, listRoute, actionName} = params
return new CRUDActions<R, L, L, S, S, S>(
http,
listRoute,
listRoute,
specificRoute,
specificRoute,
specificRoute,
actionName,
)
}
getActionTypes(): ICRUDActionTypes {
const {actionName} = this
return { return {
get: actionName + '_GET_PENDING', payload: this.http.post(this.route, params.body, params.params),
put: actionName + '_PUT_PENDING', type: this.type,
post: actionName + '_POST_PENDING', method: 'save',
delete: actionName + '_DELETE_PENDING', status: 'pending',
getMany: actionName + '_GET_MANY_PENDING',
}
}
get = (params: {
query: Optional<T[GET]['get']['query']>,
params: T[GET]['get']['params'],
}) => {
return {
payload: this.http.get(this.getRoute, params.query, params.params),
type: this.actionTypes.get,
}
}
post = (params: {
body: T[POST]['post']['body'],
params: T[POST]['post']['params'],
}) => {
return {
payload: this.http.post(this.postRoute, params.body, params.params),
type: this.actionTypes.post,
}
}
put = (params: {
body: T[PUT]['put']['body'],
params: T[PUT]['put']['params'],
}) => {
return {
payload: this.http.put(this.putRoute, params.body, params.params),
type: this.actionTypes.put,
}
}
delete = (params: {
body: T[DELETE]['delete']['body'],
params: T[DELETE]['delete']['params'],
}) => {
return {
payload: this.http.delete(this.deleteRoute, params.body, params.params),
type: this.actionTypes.delete,
}
}
getMany = (params: {
query: Optional<T[GET_MANY]['get']['query']>,
params: T[GET_MANY]['get']['params'],
}) => {
return {
payload: this.http.get(this.getManyRoute, params.query, params.params),
type: this.actionTypes.getMany,
} }
} }
} }
export class FindOneActionCreator<
T extends IRoutes,
Route extends keyof T & string,
ActionType extends string,
> {
constructor(
readonly http: IHTTPClient<T>,
readonly route: Route,
readonly type: ActionType,
) {}
findOne = (params: {
query: Optional<T[Route]['get']['query']>,
params: T[Route]['get']['params'],
}): Action<T[Route]['get']['response'], ActionType, 'findOne'> => {
return {
payload: this.http.get(this.route, params.query, params.params),
type: this.type,
method: 'findOne',
status: 'pending',
}
}
}
export class UpdateActionCreator<
T extends IRoutes,
Route extends keyof T & string,
ActionType extends string
> {
constructor(
readonly http: IHTTPClient<T>,
readonly route: Route,
readonly type: ActionType,
) {}
update = (params: {
body: T[Route]['put']['body'],
params: T[Route]['put']['params'],
}): Action<T[Route]['put']['response'], ActionType, 'update'> => {
return {
payload: this.http.put(this.route, params.body, params.params),
type: this.type,
method: 'update',
status: 'pending',
}
}
}
export class RemoveActionCreator<
T extends IRoutes,
Route extends keyof T & string,
ActionType extends string,
> {
constructor(
readonly http: IHTTPClient<T>,
readonly route: Route,
readonly type: ActionType,
) {}
remove = (params: {
body: T[Route]['delete']['body'],
params: T[Route]['delete']['params'],
}): Action<T[Route]['delete']['response'], ActionType, 'remove'> => {
return {
payload: this.http.delete(this.route, params.body, params.params),
type: this.type,
method: 'remove',
status: 'pending',
}
}
}
export class FindManyActionCreator<
T extends IRoutes,
Route extends keyof T & string,
ActionType extends string,
> {
constructor(
readonly http: IHTTPClient<T>,
readonly route: Route,
readonly type: ActionType,
) {}
findMany = (params: {
query: Optional<T[Route]['get']['query']>,
params: T[Route]['get']['params'],
}): Action<T[Route]['get']['response'], ActionType, 'findMany'> => {
return {
payload: this.http.get(this.route, params.query, params.params),
type: this.type,
method: 'findMany',
status: 'pending',
}
}
}
export function createCRUDActions<
T extends IRoutes,
EntityRoute extends keyof T & string,
ListRoute extends keyof T & string,
ActionType extends string,
>(
http: IHTTPClient<T>,
entityRoute: EntityRoute,
listRoute: ListRoute,
actionType: ActionType,
) {
const {save} = new SaveActionCreator(http, entityRoute, actionType)
const {update} = new UpdateActionCreator(http, listRoute, actionType)
const {remove} = new RemoveActionCreator(http, listRoute, actionType)
const {findOne} = new FindOneActionCreator(http, listRoute, actionType)
const {findMany} = new FindManyActionCreator(http, entityRoute, actionType)
return {save, update, remove, findOne, findMany}
}

View File

@ -1,9 +1,11 @@
import {IAction} from '../actions' import {IAction, IResolvedAction} from '../actions'
import {ICRUDAction} from './ICRUDAction'
import {ICRUDMethod} from './ICRUDMethod'
import {indexBy, without} from '@rondo/common' import {indexBy, without} from '@rondo/common'
export type ICRUDMethod = 'put' | 'post' | 'delete' | 'get' | 'getMany' type Filter<T, U> = T extends U ? T : never
export interface ICRUDIdable { export interface ICRUDEntity {
readonly id: number readonly id: number
} }
@ -12,42 +14,27 @@ export interface ICRUDMethodStatus {
readonly error: string readonly error: string
} }
export interface ICRUDState<T extends ICRUDIdable> { export interface ICRUDState<T extends ICRUDEntity> {
readonly ids: ReadonlyArray<number> readonly ids: ReadonlyArray<number>
readonly byId: Record<number, T> readonly byId: Record<number, T>
status: ICRUDStatus status: ICRUDStatus
} }
export interface ICRUDStatus { export interface ICRUDStatus {
readonly post: ICRUDMethodStatus readonly save: ICRUDMethodStatus
readonly put: ICRUDMethodStatus readonly update: ICRUDMethodStatus
readonly delete: ICRUDMethodStatus readonly remove: ICRUDMethodStatus
readonly get: ICRUDMethodStatus readonly findOne: ICRUDMethodStatus
readonly getMany: ICRUDMethodStatus readonly findMany: ICRUDMethodStatus
} }
export interface ICRUDActions { export class CRUDReducer<
readonly post: string T extends ICRUDEntity,
readonly put: string ActionType extends string,
readonly delete: string > {
readonly get: string
readonly getMany: string
}
export interface ICRUDAction<P, T extends string = string> extends IAction<T> {
payload: P,
}
export class CRUDReducer<T extends ICRUDIdable> {
readonly defaultState: ICRUDState<T> readonly defaultState: ICRUDState<T>
readonly actionTypes: ReturnType<CRUDReducer<T>['getActionTypes']>
constructor( constructor(readonly actionName: ActionType) {
readonly actionName: string,
readonly pendingExtension = '_PENDING',
readonly resolvedExtension = '_RESOLVED',
readonly rejectedExtension = '_REJECTED',
) {
const defaultMethodStatus = this.getDefaultMethodStatus() const defaultMethodStatus = this.getDefaultMethodStatus()
this.defaultState = { this.defaultState = {
@ -55,15 +42,13 @@ export class CRUDReducer<T extends ICRUDIdable> {
byId: {}, byId: {},
status: { status: {
post: defaultMethodStatus, save: defaultMethodStatus,
put: defaultMethodStatus, update: defaultMethodStatus,
delete: defaultMethodStatus, remove: defaultMethodStatus,
get: defaultMethodStatus, findOne: defaultMethodStatus,
getMany: defaultMethodStatus, findMany: defaultMethodStatus,
}, },
} }
this.actionTypes = this.getActionTypes()
} }
getDefaultMethodStatus(): ICRUDMethodStatus { getDefaultMethodStatus(): ICRUDMethodStatus {
@ -73,59 +58,6 @@ export class CRUDReducer<T extends ICRUDIdable> {
} }
} }
protected getPromiseActionNames(type: string) {
return {
pending: type + this.pendingExtension,
resolved: type + this.resolvedExtension,
rejected: type + this.rejectedExtension,
}
}
protected getActionTypes() {
const {actionName} = this
return {
put: this.getPromiseActionNames(actionName + '_PUT'),
post: this.getPromiseActionNames(actionName + '_POST'),
delete: this.getPromiseActionNames(actionName + '_DELETE'),
get: this.getPromiseActionNames(actionName + '_GET'),
getMany: this.getPromiseActionNames(actionName + '_GET_MANY'),
}
}
protected getUpdatedStatus(
state: ICRUDStatus,
method: ICRUDMethod,
status: ICRUDMethodStatus,
): ICRUDStatus {
return {
...state,
[method]: status,
}
}
protected getMethod(actionType: string): ICRUDMethod {
const {get, put, post, delete: _delete, getMany} = this.actionTypes
switch (actionType) {
case get.pending:
case get.rejected:
return 'get'
case put.pending:
case put.rejected:
return 'put'
case post.pending:
case post.rejected:
return 'post'
case _delete.pending:
case _delete.rejected:
return 'delete'
case getMany.pending:
case getMany.rejected:
return 'getMany'
default:
throw new Error('Unknown action type: ' + actionType)
}
}
protected getSuccessStatus(): ICRUDMethodStatus { protected getSuccessStatus(): ICRUDMethodStatus {
return { return {
isLoading: false, isLoading: false,
@ -133,94 +65,156 @@ export class CRUDReducer<T extends ICRUDIdable> {
} }
} }
reduce = (state: ICRUDState<T> | undefined, action: ICRUDAction<T | T[]>) handleRejected = (
: ICRUDState<T> => { state: ICRUDState<T>,
action: Filter<ICRUDAction<T, ActionType>, {status: 'rejected'}>,
): ICRUDState<T> => {
return {
...state,
status: {
...state.status,
[action.method]: {
isLoading: false,
error: action.payload.message,
},
},
}
}
handleLoading = (
state: ICRUDState<T>,
action: Filter<ICRUDAction<T, ActionType>, {status: 'pending'}>,
): ICRUDState<T> => {
return {
...state,
status: {
...state.status,
[action.method]: {
isLoading: true,
error: '',
},
},
}
}
handleFindOne = (
state: ICRUDState<T>,
action: Filter<
ICRUDAction<T, ActionType>, {method: 'findOne', status: 'resolved'}>,
): ICRUDState<T> => {
const {payload} = action
return {
...state,
ids: [...state.ids, payload.id],
byId: {
[payload.id]: payload,
},
status: {
...state.status,
[action.method]: this.getSuccessStatus(),
},
}
}
handleSave = (
state: ICRUDState<T>,
action: Filter<
ICRUDAction<T, ActionType>, {method: 'save', status: 'resolved'}>,
): ICRUDState<T> => {
const {payload} = action
return {
...state,
ids: [...state.ids, payload.id],
byId: {
[payload.id]: payload,
},
status: {
...state.status,
[action.method]: this.getSuccessStatus(),
},
}
}
handleUpdate = (
state: ICRUDState<T>,
action: Filter<
ICRUDAction<T, ActionType>, {method: 'update', status: 'resolved'}>,
): ICRUDState<T> => {
const {payload} = action
return {
...state,
byId: {
[payload.id]: payload,
},
status: {
...state.status,
[action.method]: this.getSuccessStatus(),
},
}
}
handleRemove = (
state: ICRUDState<T>,
action: Filter<
ICRUDAction<T, ActionType>, {method: 'remove', status: 'resolved'}>,
): ICRUDState<T> => {
const {payload} = action
return {
...state,
ids: state.ids.filter(id => id !== payload.id),
byId: without(state.byId, payload.id),
status: {
...state.status,
[action.method]: this.getSuccessStatus(),
},
}
}
handleFindMany = (
state: ICRUDState<T>,
action: Filter<
ICRUDAction<T, ActionType>, {method: 'findMany', status: 'resolved'}>,
): ICRUDState<T> => {
const {payload} = action
return {
...state,
ids: payload.map(item => item.id),
byId: indexBy(payload, 'id' as any),
status: {
...state.status,
[action.method]: this.getSuccessStatus(),
},
}
}
reduce = (
state: ICRUDState<T> | undefined,
action: ICRUDAction<T, ActionType>,
): ICRUDState<T> => {
const {defaultState} = this const {defaultState} = this
state = state || defaultState state = state || defaultState
const {get, put, post, delete: _delete, getMany} = this.actionTypes if (action.type !== this.actionName) {
return state
}
switch (action.type) { switch (action.status) {
case put.pending: case 'pending':
case post.pending: return this.handleLoading(state, action)
case _delete.pending: case 'rejected':
case getMany.pending: return this.handleRejected(state, action)
case get.pending: case 'resolved':
const pendingMethod = this.getMethod(action.type) switch (action.method) {
return { case 'save':
...state, return this.handleSave(state, action)
status: this.getUpdatedStatus(state.status, pendingMethod, { case 'update':
isLoading: true, return this.handleUpdate(state, action)
error: '', case 'remove':
}), return this.handleRemove(state, action)
} case 'findOne':
return this.handleFindOne(state, action)
case put.rejected: case 'findMany':
case post.rejected: return this.handleFindMany(state, action)
case _delete.rejected:
case getMany.rejected:
case get.rejected:
const rejectedMethod = this.getMethod(action.type)
const rejectedAction = action as any
return {
...state,
status: this.getUpdatedStatus(state.status, rejectedMethod, {
isLoading: false,
error: rejectedAction.error
? rejectedAction.error.message
: 'An error occurred',
}),
}
case get.resolved:
const getPayload = action.payload as T
return {
...state,
ids: [...state.ids, getPayload.id],
byId: {
[getPayload.id]: getPayload,
},
status: this.getUpdatedStatus(
state.status, 'get', this.getSuccessStatus()),
}
case post.resolved:
const postPayload = action.payload as T
return {
...state,
ids: [...state.ids, postPayload.id],
byId: {
[postPayload.id]: postPayload,
},
status: this.getUpdatedStatus(
state.status, 'post', this.getSuccessStatus()),
}
case put.resolved:
const putPayload = action.payload as T
return {
...state,
byId: {
[putPayload.id]: putPayload,
},
status: this.getUpdatedStatus(
state.status, 'put', this.getSuccessStatus()),
}
case _delete.resolved:
const deletePayload = action.payload as T
return {
...state,
ids: state.ids.filter(id => id !== deletePayload.id),
byId: without(state.byId, deletePayload.id),
status: this.getUpdatedStatus(
state.status, 'delete', this.getSuccessStatus()),
}
case getMany.resolved:
const getManyPayload = action.payload as T[]
return {
...state,
ids: getManyPayload.map(item => item.id),
byId: indexBy(getManyPayload, 'id' as any),
status: this.getUpdatedStatus(
state.status, 'getMany', this.getSuccessStatus()),
} }
default: default:
return state return state

View File

@ -0,0 +1,24 @@
import {IAsyncAction} from '../actions'
import {ICRUDMethod} from './ICRUDMethod'
export type ICRUDSaveAction<T, ActionType extends string> =
IAsyncAction<T, ActionType> & {method: Extract<ICRUDMethod, 'save'>}
export type ICRUDUpdateAction<T, ActionType extends string> =
IAsyncAction<T, ActionType> & {method: Extract<ICRUDMethod, 'update'>}
export type ICRUDRemoveAction<T, ActionType extends string> =
IAsyncAction<T, ActionType> & {method: Extract<ICRUDMethod, 'remove'>}
export type ICRUDFindOneAction<T, ActionType extends string> =
IAsyncAction<T, ActionType> & {method: Extract<ICRUDMethod, 'findOne'>}
export type ICRUDFindManyAction<T, ActionType extends string> =
IAsyncAction<T[], ActionType> & {method: Extract<ICRUDMethod, 'findMany'>}
export type ICRUDAction<T, ActionType extends string> =
ICRUDSaveAction<T, ActionType>
| ICRUDUpdateAction<T, ActionType>
| ICRUDRemoveAction<T, ActionType>
| ICRUDFindOneAction<T, ActionType>
| ICRUDFindManyAction<T, ActionType>

View File

@ -0,0 +1 @@
export type ICRUDMethod = 'save' | 'update' | 'findOne' | 'findMany' | 'remove'

View File

@ -1,2 +1,4 @@
export * from './CRUDActions' export * from './CRUDActions'
export * from './CRUDReducer' export * from './CRUDReducer'
export * from './ICRUDAction'
export * from './ICRUDMethod'

View File

@ -26,6 +26,7 @@ export function Login(
default: default:
// async actions // async actions
switch (action.status) { switch (action.status) {
// FIXME this will trigger for all async actions with status pending
case 'pending': case 'pending':
return { return {
...state, ...state,

View File

@ -64,7 +64,7 @@ describe('PromiseMiddleware', () => {
status: 'pending', status: 'pending',
type, type,
}, { }, {
error, payload: error,
status: 'rejected', status: 'rejected',
type, type,
}]) }])

View File

@ -35,16 +35,16 @@ export class PromiseMiddleware {
payload payload
.then(result => { .then(result => {
store.dispatch({ store.dispatch({
...action,
payload: result, payload: result,
status: 'resolved', status: 'resolved',
type,
}) })
}) })
.catch(err => { .catch(err => {
store.dispatch({ store.dispatch({
error: err, ...action,
payload: err,
status: 'rejected', status: 'rejected',
type,
}) })
}) })