Add packages/client/src/crud
This commit is contained in:
parent
eed79f35f2
commit
73be64a900
@ -1,7 +1,9 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {AnyAction} from 'redux'
|
import {AnyAction} from 'redux'
|
||||||
import {CRUDActions, CRUDReducer} from './'
|
import {CRUDActions, CRUDReducer, ICRUDMethod} from './'
|
||||||
import {HTTPClientMock, TestUtils, getError} from '../test-utils'
|
import {HTTPClientMock, TestUtils, getError} from '../test-utils'
|
||||||
|
import {IMethod} from '@rondo/common'
|
||||||
|
import {IPendingAction} from '../actions'
|
||||||
|
|
||||||
describe('CRUD', () => {
|
describe('CRUD', () => {
|
||||||
|
|
||||||
@ -27,7 +29,6 @@ describe('CRUD', () => {
|
|||||||
'/one/:oneId/two/:twoId': {
|
'/one/:oneId/two/:twoId': {
|
||||||
get: {
|
get: {
|
||||||
params: ITwoSpecificParams
|
params: ITwoSpecificParams
|
||||||
body: ITwoCreateBody
|
|
||||||
response: ITwo
|
response: ITwo
|
||||||
}
|
}
|
||||||
put: {
|
put: {
|
||||||
@ -37,7 +38,7 @@ describe('CRUD', () => {
|
|||||||
}
|
}
|
||||||
delete: {
|
delete: {
|
||||||
params: ITwoSpecificParams
|
params: ITwoSpecificParams
|
||||||
response: {id: number}
|
response: {id: number} // TODO return ITwoSpecificParams
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'/one/:oneId/two': {
|
'/one/:oneId/two': {
|
||||||
@ -60,7 +61,8 @@ describe('CRUD', () => {
|
|||||||
specificRoute: '/one/:oneId/two/:twoId',
|
specificRoute: '/one/:oneId/two/:twoId',
|
||||||
actionName: 'TEST',
|
actionName: 'TEST',
|
||||||
})
|
})
|
||||||
const Crud = new CRUDReducer<ITwo>('TEST').reduce
|
const crudReducer = new CRUDReducer<ITwo>('TEST')
|
||||||
|
const Crud = crudReducer.reduce
|
||||||
|
|
||||||
const test = new TestUtils()
|
const test = new TestUtils()
|
||||||
const reducer = test.combineReducers({
|
const reducer = test.combineReducers({
|
||||||
@ -73,6 +75,8 @@ describe('CRUD', () => {
|
|||||||
return test.createStore({reducer})()
|
return test.createStore({reducer})()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Store = ReturnType<typeof getStore>
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
http.mockClear()
|
http.mockClear()
|
||||||
})
|
})
|
||||||
@ -83,74 +87,214 @@ describe('CRUD', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('GET_MANY', () => {
|
function expectActions(actionTypes: string[]) {
|
||||||
|
|
||||||
function getAction(store: ReturnType<typeof getStore>) {
|
|
||||||
const action = store.dispatch(actions.getMany({
|
|
||||||
query: {},
|
|
||||||
params: {oneId: 1},
|
|
||||||
}))
|
|
||||||
const state = store.getState()
|
const state = store.getState()
|
||||||
expect(state.Crud.status.getMany.isLoading).toBe(true)
|
// first action is redux initializer
|
||||||
expect(state.Logger).toEqual([
|
expect(state.Logger.slice(1)).toEqual(actionTypes)
|
||||||
jasmine.any(String),
|
}
|
||||||
'TEST_GET_MANY_PENDING',
|
|
||||||
])
|
let store: Store
|
||||||
|
beforeEach(() => {
|
||||||
|
store = getStore()
|
||||||
|
})
|
||||||
|
|
||||||
|
function dispatch(
|
||||||
|
method: ICRUDMethod,
|
||||||
|
action: IPendingAction<unknown, string>,
|
||||||
|
) {
|
||||||
|
store.dispatch(action)
|
||||||
|
expect(store.getState().Crud.status[method].isLoading).toBe(true)
|
||||||
|
expectActions([action.type])
|
||||||
return action
|
return action
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('TEST_GET_MANY_RESOLVED', () => {
|
function getUrl(method: ICRUDMethod) {
|
||||||
|
return method === 'post' || method === 'getMany'
|
||||||
|
? '/one/1/two'
|
||||||
|
: '/one/1/two/2'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHTTPMethod(method: ICRUDMethod): IMethod {
|
||||||
|
return method === 'getMany' ? 'get' : method
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Promise rejections', () => {
|
||||||
|
const testCases: Array<{
|
||||||
|
method: ICRUDMethod
|
||||||
|
params: any
|
||||||
|
}> = [{
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
params: {oneId: 1, twoId: 2},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
method: 'getMany',
|
||||||
|
params: {
|
||||||
|
params: {oneId: 1},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
method: 'post',
|
||||||
|
params: {
|
||||||
|
body: {name: 'test'},
|
||||||
|
params: {oneId: 1, twoId: 2},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
method: 'put',
|
||||||
|
params: {
|
||||||
|
body: {name: 'test'},
|
||||||
|
params: {oneId: 1, twoId: 2},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
method: 'delete',
|
||||||
|
params: {
|
||||||
|
body: {},
|
||||||
|
params: {oneId: 1, twoId: 2},
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
testCases.forEach(testCase => {
|
||||||
|
|
||||||
|
const {method} = testCase
|
||||||
|
describe(method, () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
http.mockAdd({
|
http.mockAdd({
|
||||||
method: 'get',
|
url: getUrl(method),
|
||||||
url: '/one/1/two',
|
method: getHTTPMethod(method),
|
||||||
params: {},
|
data: method === 'put' || method === 'post' || method === 'delete'
|
||||||
}, [{id: 2, name: 'bla'}])
|
? testCase.params.body
|
||||||
})
|
: undefined,
|
||||||
|
|
||||||
it('updates state', async () => {
|
|
||||||
const store = getStore()
|
|
||||||
const action = getAction(store)
|
|
||||||
await action.payload
|
|
||||||
const state = store.getState()
|
|
||||||
expect(state.Crud.status.getMany.isLoading).toBe(false)
|
|
||||||
expect(state.Crud.ids).toEqual([2])
|
|
||||||
expect(state.Crud.byId[2]).toEqual({id: 2, name: 'bla'})
|
|
||||||
expect(state.Logger).toEqual([
|
|
||||||
jasmine.any(String),
|
|
||||||
'TEST_GET_MANY_PENDING',
|
|
||||||
'TEST_GET_MANY_RESOLVED',
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('TEST_GET_MANY_REJECTED', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
http.mockAdd({
|
|
||||||
method: 'get',
|
|
||||||
url: '/one/1/two',
|
|
||||||
params: {},
|
|
||||||
}, {error: 'Test Error'}, 400)
|
}, {error: 'Test Error'}, 400)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('updates state', async () => {
|
it(`updates status on error: ${method}`, async () => {
|
||||||
const store = getStore()
|
const action = actions[method](testCase.params)
|
||||||
const action = getAction(store)
|
dispatch(testCase.method, action)
|
||||||
await getError(action.payload)
|
await getError(action.payload)
|
||||||
const state = store.getState()
|
const state = store.getState()
|
||||||
expect(state.Crud.status.getMany.isLoading).toBe(false)
|
|
||||||
// TODO use error from response
|
|
||||||
expect(state.Crud.status.getMany.error).toBe('HTTP Status: 400')
|
|
||||||
expect(state.Crud.ids).toEqual([])
|
|
||||||
expect(state.Crud.byId).toEqual({})
|
expect(state.Crud.byId).toEqual({})
|
||||||
expect(state.Logger).toEqual([
|
expect(state.Crud.ids).toEqual([])
|
||||||
jasmine.any(String),
|
expect(state.Crud.status[method].isLoading).toBe(false)
|
||||||
'TEST_GET_MANY_PENDING',
|
// TODO use error from response
|
||||||
'TEST_GET_MANY_REJECTED',
|
expect(state.Crud.status[method].error).toEqual('HTTP Status: 400')
|
||||||
|
expectActions([
|
||||||
|
action.type,
|
||||||
|
action.type.replace(/_PENDING$/, '_REJECTED'),
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Resolved promises', () => {
|
||||||
|
const entity = {id: 100, name: 'test'}
|
||||||
|
|
||||||
|
const testCases: Array<{
|
||||||
|
method: ICRUDMethod
|
||||||
|
params: any
|
||||||
|
body?: any
|
||||||
|
response: any
|
||||||
|
}> = [{
|
||||||
|
method: 'getMany',
|
||||||
|
params: {oneId: 1, twoId: 2},
|
||||||
|
response: [entity],
|
||||||
|
}, {
|
||||||
|
method: 'get',
|
||||||
|
params: {oneId: 1, twoId: 2},
|
||||||
|
response: entity,
|
||||||
|
}, {
|
||||||
|
method: 'post',
|
||||||
|
params: {oneId: 1},
|
||||||
|
body: {name: entity.name},
|
||||||
|
response: entity,
|
||||||
|
}, {
|
||||||
|
method: 'put',
|
||||||
|
params: {oneId: 1, twoId: 2},
|
||||||
|
body: {name: entity.name},
|
||||||
|
response: entity,
|
||||||
|
}, {
|
||||||
|
method: 'delete',
|
||||||
|
params: {oneId: 1, twoId: 2},
|
||||||
|
response: {id: entity.id},
|
||||||
|
}]
|
||||||
|
|
||||||
|
testCases.forEach(testCase => {
|
||||||
|
const {method} = testCase
|
||||||
|
|
||||||
|
describe(method, () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
http.mockAdd({
|
||||||
|
url: getUrl(method),
|
||||||
|
method: getHTTPMethod(method),
|
||||||
|
data: testCase.body,
|
||||||
|
}, testCase.response)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
http.mockClear()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('updates state', async () => {
|
||||||
|
const action = dispatch(testCase.method, actions[method]({
|
||||||
|
query: undefined,
|
||||||
|
params: testCase.params,
|
||||||
|
body: testCase.body,
|
||||||
|
}))
|
||||||
|
await action.payload
|
||||||
|
const state = store.getState()
|
||||||
|
expect(state.Crud.status.getMany.isLoading).toBe(false)
|
||||||
|
if (method === 'delete') {
|
||||||
|
expect(state.Crud.ids).toEqual([])
|
||||||
|
expect(state.Crud.byId[entity.id]).toBe(undefined)
|
||||||
|
} else {
|
||||||
|
if (method !== 'put') {
|
||||||
|
expect(state.Crud.ids).toEqual([entity.id])
|
||||||
|
}
|
||||||
|
expect(state.Crud.byId[entity.id]).toEqual(entity)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('POST then DELETE', () => {
|
||||||
|
|
||||||
|
const postTestCase = testCases.find(t => t.method === 'post')!
|
||||||
|
const deleteTestCase = testCases.find(t => t.method === 'delete')!
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
http.mockAdd({
|
||||||
|
url: getUrl(postTestCase.method),
|
||||||
|
method: getHTTPMethod(postTestCase.method),
|
||||||
|
data: postTestCase.body,
|
||||||
|
}, postTestCase.response)
|
||||||
|
http.mockAdd({
|
||||||
|
url: getUrl(deleteTestCase.method),
|
||||||
|
method: getHTTPMethod(deleteTestCase.method),
|
||||||
|
data: deleteTestCase.body,
|
||||||
|
}, deleteTestCase.response)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
http.mockClear()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('removes id and entity from state', async () => {
|
||||||
|
const action1 = store.dispatch(actions.post({
|
||||||
|
params: postTestCase.params,
|
||||||
|
body: postTestCase.body,
|
||||||
|
}))
|
||||||
|
await action1.payload
|
||||||
|
expect(store.getState().Crud.ids).toEqual([entity.id])
|
||||||
|
const action2 = store.dispatch(actions.delete({
|
||||||
|
params: deleteTestCase.params,
|
||||||
|
body: deleteTestCase.body,
|
||||||
|
}))
|
||||||
|
await action2.payload
|
||||||
|
expect(store.getState().Crud.ids).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import {IRoutes} from '@rondo/common'
|
import {IRoutes} from '@rondo/common'
|
||||||
import {IHTTPClient, ITypedRequestParams} from '../http'
|
import {IHTTPClient, ITypedRequestParams} from '../http'
|
||||||
|
|
||||||
|
export type Optional<T> = T extends {} ? T : undefined
|
||||||
|
|
||||||
interface ICRUDActionTypes {
|
interface ICRUDActionTypes {
|
||||||
readonly get: string
|
readonly get: string
|
||||||
readonly put: string
|
readonly put: string
|
||||||
@ -66,7 +68,7 @@ export class CRUDActions<
|
|||||||
}
|
}
|
||||||
|
|
||||||
get(params: {
|
get(params: {
|
||||||
query: T[GET]['get']['query'],
|
query: Optional<T[GET]['get']['query']>,
|
||||||
params: T[GET]['get']['params'],
|
params: T[GET]['get']['params'],
|
||||||
}) {
|
}) {
|
||||||
return {
|
return {
|
||||||
@ -106,7 +108,7 @@ export class CRUDActions<
|
|||||||
}
|
}
|
||||||
|
|
||||||
getMany(params: {
|
getMany(params: {
|
||||||
query: T[GET_MANY]['get']['query'],
|
query: Optional<T[GET_MANY]['get']['query']>,
|
||||||
params: T[GET_MANY]['get']['params'],
|
params: T[GET_MANY]['get']['params'],
|
||||||
}) {
|
}) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -76,7 +76,7 @@ export class CRUDReducer<T extends ICRUDIdable> {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
this.actionTypes = this.getActionTypes(actionName)
|
this.actionTypes = this.getActionTypes()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getPromiseActionNames(type: string) {
|
protected getPromiseActionNames(type: string) {
|
||||||
@ -87,7 +87,8 @@ export class CRUDReducer<T extends ICRUDIdable> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getActionTypes(actionName: string) {
|
protected getActionTypes() {
|
||||||
|
const {actionName} = this
|
||||||
return {
|
return {
|
||||||
put: this.getPromiseActionNames(actionName + '_PUT'),
|
put: this.getPromiseActionNames(actionName + '_PUT'),
|
||||||
post: this.getPromiseActionNames(actionName + '_POST'),
|
post: this.getPromiseActionNames(actionName + '_POST'),
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
export interface IHeader {
|
export interface IHeader {
|
||||||
[key: string]: string
|
readonly [key: string]: string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,7 +49,7 @@ export class HTTPClientMock<T extends IRoutes> extends HTTPClient<T> {
|
|||||||
if (!this.mocks.hasOwnProperty(key)) {
|
if (!this.mocks.hasOwnProperty(key)) {
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
const err = new Error(
|
const err = new Error(
|
||||||
'No mock for request: ' + key + '\nAvailable mocks:' +
|
'No mock for request: ' + key + '\nAvailable mocks: ' +
|
||||||
Object.keys(this.mocks))
|
Object.keys(this.mocks))
|
||||||
reject(err)
|
reject(err)
|
||||||
currentRequest.finished = true
|
currentRequest.finished = true
|
||||||
@ -76,7 +76,12 @@ export class HTTPClientMock<T extends IRoutes> extends HTTPClient<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected serialize(req: IRequest) {
|
protected serialize(req: IRequest) {
|
||||||
return JSON.stringify(req, null, ' ')
|
return JSON.stringify({
|
||||||
|
method: req.method,
|
||||||
|
url: req.url,
|
||||||
|
params: req.params,
|
||||||
|
data: req.data,
|
||||||
|
}, null, ' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user