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 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([])
|
||||||
|
|||||||
@ -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}
|
||||||
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
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 './CRUDActions'
|
||||||
export * from './CRUDReducer'
|
export * from './CRUDReducer'
|
||||||
|
export * from './ICRUDAction'
|
||||||
|
export * from './ICRUDMethod'
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -64,7 +64,7 @@ describe('PromiseMiddleware', () => {
|
|||||||
status: 'pending',
|
status: 'pending',
|
||||||
type,
|
type,
|
||||||
}, {
|
}, {
|
||||||
error,
|
payload: error,
|
||||||
status: 'rejected',
|
status: 'rejected',
|
||||||
type,
|
type,
|
||||||
}])
|
}])
|
||||||
|
|||||||
@ -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,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user