Simplify types for CRUDActions & CRUDReducer
This commit is contained in:
parent
8b6f90235e
commit
580fb368e6
@ -1,6 +1,7 @@
|
||||
import {createCRUDActions} from './CRUDActions'
|
||||
import React from 'react'
|
||||
import {AnyAction} from 'redux'
|
||||
import {CRUDActions, CRUDReducer, ICRUDMethod} from './'
|
||||
import {CRUDReducer, ICRUDMethod} from './'
|
||||
import {HTTPClientMock, TestUtils, getError} from '../test-utils'
|
||||
import {IMethod} from '@rondo/common'
|
||||
import {IPendingAction} from '../actions'
|
||||
@ -55,13 +56,13 @@ describe('CRUD', () => {
|
||||
}
|
||||
|
||||
const http = new HTTPClientMock<ITestAPI>()
|
||||
const actions = CRUDActions.fromTwoRoutes({
|
||||
const actions = createCRUDActions(
|
||||
http,
|
||||
listRoute: '/one/:oneId/two',
|
||||
specificRoute: '/one/:oneId/two/:twoId',
|
||||
actionName: 'TEST',
|
||||
})
|
||||
const crudReducer = new CRUDReducer<ITwo>('TEST')
|
||||
'/one/:oneId/two',
|
||||
'/one/:oneId/two/:twoId',
|
||||
'TEST',
|
||||
)
|
||||
const crudReducer = new CRUDReducer<ITwo, 'TEST'>('TEST')
|
||||
const Crud = crudReducer.reduce
|
||||
|
||||
const test = new TestUtils()
|
||||
@ -109,13 +110,23 @@ describe('CRUD', () => {
|
||||
}
|
||||
|
||||
function getUrl(method: ICRUDMethod) {
|
||||
return method === 'post' || method === 'getMany'
|
||||
return method === 'save' || method === 'findMany'
|
||||
? '/one/1/two'
|
||||
: '/one/1/two/2'
|
||||
}
|
||||
|
||||
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', () => {
|
||||
@ -123,29 +134,29 @@ describe('CRUD', () => {
|
||||
method: ICRUDMethod
|
||||
params: any
|
||||
}> = [{
|
||||
method: 'get',
|
||||
method: 'findOne',
|
||||
params: {
|
||||
params: {oneId: 1, twoId: 2},
|
||||
},
|
||||
}, {
|
||||
method: 'getMany',
|
||||
method: 'findMany',
|
||||
params: {
|
||||
params: {oneId: 1},
|
||||
},
|
||||
}, {
|
||||
method: 'post',
|
||||
method: 'save',
|
||||
params: {
|
||||
body: {name: 'test'},
|
||||
params: {oneId: 1, twoId: 2},
|
||||
},
|
||||
}, {
|
||||
method: 'put',
|
||||
method: 'update',
|
||||
params: {
|
||||
body: {name: 'test'},
|
||||
params: {oneId: 1, twoId: 2},
|
||||
},
|
||||
}, {
|
||||
method: 'delete',
|
||||
method: 'remove',
|
||||
params: {
|
||||
body: {},
|
||||
params: {oneId: 1, twoId: 2},
|
||||
@ -160,9 +171,10 @@ describe('CRUD', () => {
|
||||
http.mockAdd({
|
||||
url: getUrl(method),
|
||||
method: getHTTPMethod(method),
|
||||
data: method === 'put' || method === 'post' || method === 'delete'
|
||||
data: method === 'save'
|
||||
|| method === 'update' || method === 'remove'
|
||||
? testCase.params.body
|
||||
: undefined,
|
||||
: undefined,
|
||||
}, {error: 'Test Error'}, 400)
|
||||
})
|
||||
|
||||
@ -196,25 +208,25 @@ describe('CRUD', () => {
|
||||
body?: any
|
||||
response: any
|
||||
}> = [{
|
||||
method: 'getMany',
|
||||
method: 'findMany',
|
||||
params: {oneId: 1, twoId: 2},
|
||||
response: [entity],
|
||||
}, {
|
||||
method: 'get',
|
||||
method: 'findOne',
|
||||
params: {oneId: 1, twoId: 2},
|
||||
response: entity,
|
||||
}, {
|
||||
method: 'post',
|
||||
method: 'save',
|
||||
params: {oneId: 1},
|
||||
body: {name: entity.name},
|
||||
response: entity,
|
||||
}, {
|
||||
method: 'put',
|
||||
method: 'update',
|
||||
params: {oneId: 1, twoId: 2},
|
||||
body: {name: entity.name},
|
||||
response: entity,
|
||||
}, {
|
||||
method: 'delete',
|
||||
method: 'remove',
|
||||
params: {oneId: 1, twoId: 2},
|
||||
response: {id: entity.id},
|
||||
}]
|
||||
@ -243,12 +255,12 @@ describe('CRUD', () => {
|
||||
}))
|
||||
await action.payload
|
||||
const state = store.getState()
|
||||
expect(state.Crud.status.getMany.isLoading).toBe(false)
|
||||
if (method === 'delete') {
|
||||
expect(state.Crud.status.findMany.isLoading).toBe(false)
|
||||
if (method === 'remove') {
|
||||
expect(state.Crud.ids).toEqual([])
|
||||
expect(state.Crud.byId[entity.id]).toBe(undefined)
|
||||
} else {
|
||||
if (method !== 'put') {
|
||||
if (method !== 'update') {
|
||||
expect(state.Crud.ids).toEqual([entity.id])
|
||||
}
|
||||
expect(state.Crud.byId[entity.id]).toEqual(entity)
|
||||
@ -259,20 +271,20 @@ describe('CRUD', () => {
|
||||
|
||||
describe('POST then DELETE', () => {
|
||||
|
||||
const postTestCase = testCases.find(t => t.method === 'post')!
|
||||
const deleteTestCase = testCases.find(t => t.method === 'delete')!
|
||||
const saveTestCase = testCases.find(t => t.method === 'save')!
|
||||
const removeTestCase = testCases.find(t => t.method === 'remove')!
|
||||
|
||||
beforeEach(() => {
|
||||
http.mockAdd({
|
||||
url: getUrl(postTestCase.method),
|
||||
method: getHTTPMethod(postTestCase.method),
|
||||
data: postTestCase.body,
|
||||
}, postTestCase.response)
|
||||
url: getUrl(saveTestCase.method),
|
||||
method: getHTTPMethod(saveTestCase.method),
|
||||
data: saveTestCase.body,
|
||||
}, saveTestCase.response)
|
||||
http.mockAdd({
|
||||
url: getUrl(deleteTestCase.method),
|
||||
method: getHTTPMethod(deleteTestCase.method),
|
||||
data: deleteTestCase.body,
|
||||
}, deleteTestCase.response)
|
||||
url: getUrl(removeTestCase.method),
|
||||
method: getHTTPMethod(removeTestCase.method),
|
||||
data: removeTestCase.body,
|
||||
}, removeTestCase.response)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@ -280,15 +292,15 @@ describe('CRUD', () => {
|
||||
})
|
||||
|
||||
it('removes id and entity from state', async () => {
|
||||
const action1 = store.dispatch(actions.post({
|
||||
params: postTestCase.params,
|
||||
body: postTestCase.body,
|
||||
const action1 = store.dispatch(actions.save({
|
||||
params: saveTestCase.params,
|
||||
body: saveTestCase.body,
|
||||
}))
|
||||
await action1.payload
|
||||
expect(store.getState().Crud.ids).toEqual([entity.id])
|
||||
const action2 = store.dispatch(actions.delete({
|
||||
params: deleteTestCase.params,
|
||||
body: deleteTestCase.body,
|
||||
const action2 = store.dispatch(actions.remove({
|
||||
params: removeTestCase.params,
|
||||
body: removeTestCase.body,
|
||||
}))
|
||||
await action2.payload
|
||||
expect(store.getState().Crud.ids).toEqual([])
|
||||
|
||||
@ -1,119 +1,159 @@
|
||||
import {IRoutes} from '@rondo/common'
|
||||
import {ICRUDAction} from './ICRUDAction'
|
||||
import {ICRUDMethod} from './ICRUDMethod'
|
||||
import {IHTTPClient, ITypedRequestParams} from '../http'
|
||||
import {IRoutes} from '@rondo/common'
|
||||
|
||||
export type Optional<T> = T extends {} ? T : undefined
|
||||
|
||||
interface ICRUDActionTypes {
|
||||
readonly get: string
|
||||
readonly put: string
|
||||
readonly post: string
|
||||
readonly delete: string
|
||||
readonly getMany: string
|
||||
}
|
||||
type Filter<T, U> = T extends U ? T : never
|
||||
|
||||
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,
|
||||
POST extends keyof T & string,
|
||||
GET_MANY extends keyof T & string,
|
||||
GET extends keyof T & string,
|
||||
PUT extends keyof T & string,
|
||||
DELETE extends keyof T & string,
|
||||
Route extends keyof T & string,
|
||||
ActionType extends string,
|
||||
> {
|
||||
readonly actionTypes: ICRUDActionTypes
|
||||
|
||||
constructor(
|
||||
readonly http: IHTTPClient<T>,
|
||||
readonly postRoute: POST,
|
||||
readonly getManyRoute: GET_MANY,
|
||||
readonly getRoute: GET,
|
||||
readonly putRoute: PUT,
|
||||
readonly deleteRoute: DELETE,
|
||||
readonly actionName: string,
|
||||
) {
|
||||
this.actionTypes = this.getActionTypes()
|
||||
}
|
||||
readonly route: Route,
|
||||
readonly type: ActionType,
|
||||
) {}
|
||||
|
||||
static fromTwoRoutes<
|
||||
R,
|
||||
S extends keyof R & string,
|
||||
L extends keyof R & string,
|
||||
>(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
|
||||
save = (params: {
|
||||
body: T[Route]['post']['body'],
|
||||
params: T[Route]['post']['params'],
|
||||
}): Action<T[Route]['post']['response'], ActionType, 'save'> => {
|
||||
return {
|
||||
get: actionName + '_GET_PENDING',
|
||||
put: actionName + '_PUT_PENDING',
|
||||
post: actionName + '_POST_PENDING',
|
||||
delete: actionName + '_DELETE_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,
|
||||
payload: this.http.post(this.route, params.body, params.params),
|
||||
type: this.type,
|
||||
method: 'save',
|
||||
status: 'pending',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
@ -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'
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -12,42 +14,27 @@ export interface ICRUDMethodStatus {
|
||||
readonly error: string
|
||||
}
|
||||
|
||||
export interface ICRUDState<T extends ICRUDIdable> {
|
||||
export interface ICRUDState<T extends ICRUDEntity> {
|
||||
readonly ids: ReadonlyArray<number>
|
||||
readonly byId: Record<number, T>
|
||||
status: ICRUDStatus
|
||||
}
|
||||
|
||||
export interface ICRUDStatus {
|
||||
readonly post: ICRUDMethodStatus
|
||||
readonly put: ICRUDMethodStatus
|
||||
readonly delete: ICRUDMethodStatus
|
||||
readonly get: ICRUDMethodStatus
|
||||
readonly getMany: ICRUDMethodStatus
|
||||
readonly save: ICRUDMethodStatus
|
||||
readonly update: ICRUDMethodStatus
|
||||
readonly remove: ICRUDMethodStatus
|
||||
readonly findOne: ICRUDMethodStatus
|
||||
readonly findMany: ICRUDMethodStatus
|
||||
}
|
||||
|
||||
export interface ICRUDActions {
|
||||
readonly post: string
|
||||
readonly put: 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> {
|
||||
export class CRUDReducer<
|
||||
T extends ICRUDEntity,
|
||||
ActionType extends string,
|
||||
> {
|
||||
readonly defaultState: ICRUDState<T>
|
||||
readonly actionTypes: ReturnType<CRUDReducer<T>['getActionTypes']>
|
||||
|
||||
constructor(
|
||||
readonly actionName: string,
|
||||
readonly pendingExtension = '_PENDING',
|
||||
readonly resolvedExtension = '_RESOLVED',
|
||||
readonly rejectedExtension = '_REJECTED',
|
||||
) {
|
||||
constructor(readonly actionName: ActionType) {
|
||||
|
||||
const defaultMethodStatus = this.getDefaultMethodStatus()
|
||||
this.defaultState = {
|
||||
@ -55,15 +42,13 @@ export class CRUDReducer<T extends ICRUDIdable> {
|
||||
byId: {},
|
||||
|
||||
status: {
|
||||
post: defaultMethodStatus,
|
||||
put: defaultMethodStatus,
|
||||
delete: defaultMethodStatus,
|
||||
get: defaultMethodStatus,
|
||||
getMany: defaultMethodStatus,
|
||||
save: defaultMethodStatus,
|
||||
update: defaultMethodStatus,
|
||||
remove: defaultMethodStatus,
|
||||
findOne: defaultMethodStatus,
|
||||
findMany: defaultMethodStatus,
|
||||
},
|
||||
}
|
||||
|
||||
this.actionTypes = this.getActionTypes()
|
||||
}
|
||||
|
||||
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 {
|
||||
return {
|
||||
isLoading: false,
|
||||
@ -133,94 +65,156 @@ export class CRUDReducer<T extends ICRUDIdable> {
|
||||
}
|
||||
}
|
||||
|
||||
reduce = (state: ICRUDState<T> | undefined, action: ICRUDAction<T | T[]>)
|
||||
: ICRUDState<T> => {
|
||||
handleRejected = (
|
||||
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
|
||||
state = state || defaultState
|
||||
|
||||
const {get, put, post, delete: _delete, getMany} = this.actionTypes
|
||||
if (action.type !== this.actionName) {
|
||||
return state
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case put.pending:
|
||||
case post.pending:
|
||||
case _delete.pending:
|
||||
case getMany.pending:
|
||||
case get.pending:
|
||||
const pendingMethod = this.getMethod(action.type)
|
||||
return {
|
||||
...state,
|
||||
status: this.getUpdatedStatus(state.status, pendingMethod, {
|
||||
isLoading: true,
|
||||
error: '',
|
||||
}),
|
||||
}
|
||||
|
||||
case put.rejected:
|
||||
case post.rejected:
|
||||
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()),
|
||||
switch (action.status) {
|
||||
case 'pending':
|
||||
return this.handleLoading(state, action)
|
||||
case 'rejected':
|
||||
return this.handleRejected(state, action)
|
||||
case 'resolved':
|
||||
switch (action.method) {
|
||||
case 'save':
|
||||
return this.handleSave(state, action)
|
||||
case 'update':
|
||||
return this.handleUpdate(state, action)
|
||||
case 'remove':
|
||||
return this.handleRemove(state, action)
|
||||
case 'findOne':
|
||||
return this.handleFindOne(state, action)
|
||||
case 'findMany':
|
||||
return this.handleFindMany(state, action)
|
||||
}
|
||||
default:
|
||||
return state
|
||||
|
||||
24
packages/client/src/crud/ICRUDAction.ts
Normal file
24
packages/client/src/crud/ICRUDAction.ts
Normal 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>
|
||||
1
packages/client/src/crud/ICRUDMethod.ts
Normal file
1
packages/client/src/crud/ICRUDMethod.ts
Normal file
@ -0,0 +1 @@
|
||||
export type ICRUDMethod = 'save' | 'update' | 'findOne' | 'findMany' | 'remove'
|
||||
@ -1,2 +1,4 @@
|
||||
export * from './CRUDActions'
|
||||
export * from './CRUDReducer'
|
||||
export * from './ICRUDAction'
|
||||
export * from './ICRUDMethod'
|
||||
|
||||
@ -26,6 +26,7 @@ export function Login(
|
||||
default:
|
||||
// async actions
|
||||
switch (action.status) {
|
||||
// FIXME this will trigger for all async actions with status pending
|
||||
case 'pending':
|
||||
return {
|
||||
...state,
|
||||
|
||||
@ -64,7 +64,7 @@ describe('PromiseMiddleware', () => {
|
||||
status: 'pending',
|
||||
type,
|
||||
}, {
|
||||
error,
|
||||
payload: error,
|
||||
status: 'rejected',
|
||||
type,
|
||||
}])
|
||||
|
||||
@ -35,16 +35,16 @@ export class PromiseMiddleware {
|
||||
payload
|
||||
.then(result => {
|
||||
store.dispatch({
|
||||
...action,
|
||||
payload: result,
|
||||
status: 'resolved',
|
||||
type,
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
store.dispatch({
|
||||
error: err,
|
||||
...action,
|
||||
payload: err,
|
||||
status: 'rejected',
|
||||
type,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user