├── .nvmrc ├── .npmignore ├── src ├── configManager │ ├── index.ts │ ├── interfaces │ │ ├── index.ts │ │ └── configManager.ts │ ├── configManager.ts │ └── __tests__ │ │ └── configManager-test.ts ├── errors │ ├── interfaces │ │ ├── index.ts │ │ └── ApiError.ts │ ├── index.ts │ ├── ApiError.ts │ └── __tests__ │ │ └── ApiError-test.ts ├── crudEntity │ ├── interfaces │ │ ├── index.ts │ │ └── entities.ts │ ├── index.ts │ ├── formatEntities.ts │ └── __tests__ │ │ └── formatEntities-test.ts ├── epicFactory │ ├── interfaces │ │ ├── index.ts │ │ └── epicFactory.ts │ ├── index.ts │ ├── computeApiConfig.ts │ ├── epicFactory.ts │ ├── __tests__ │ │ ├── computeApiConfig-test.ts │ │ └── epicFactory-test.ts │ ├── createEpicFactory.ts │ ├── updateEpicFactory.ts │ ├── deleteEpicFactory.ts │ └── readEpicFactory.ts ├── constantFactory │ ├── interfaces │ │ ├── index.ts │ │ └── ICrudAction.ts │ ├── store.ts │ ├── index.ts │ ├── __tests__ │ │ ├── store-test.ts │ │ ├── create-test.ts │ │ ├── update-test.ts │ │ ├── delete-test.ts │ │ └── read-test.ts │ ├── create.ts │ ├── update.ts │ ├── delete.ts │ └── read.ts ├── selectorFactory │ ├── interfaces │ │ ├── index.ts │ │ └── selectorFactory.ts │ ├── index.ts │ ├── selectorFactory.ts │ └── __tests__ │ │ └── selectorFactory-test.ts ├── reducerFactory │ ├── interfaces │ │ ├── index.ts │ │ ├── initialState.ts │ │ └── reducerFactory.ts │ ├── index.ts │ ├── initialState.ts │ ├── computeInitialState.ts │ ├── storeHandlersFactory.ts │ ├── createHandlersFactory.ts │ ├── updateHandlersFactory.ts │ ├── __tests__ │ │ ├── computeInitialState-test.ts │ │ └── reducerFactory-test.ts │ ├── deleteHandlersFactory.ts │ ├── reducerFactory.ts │ └── readHandlersFactory.ts ├── observableApiConnector │ ├── index.ts │ ├── interfaces │ │ ├── index.ts │ │ ├── streamFormatters.ts │ │ ├── observableApiConnector.ts │ │ └── requestFormatters.ts │ ├── streamFormatters.ts │ ├── requestFormatters.ts │ ├── observableApiConnector.ts │ └── __tests__ │ │ ├── requestFormatters-test.ts │ │ ├── streamFormatters-test.ts │ │ ├── observableApiConnector-test.ts │ │ └── observableApiConnector-func-test.ts ├── actionsCreatorFactory │ ├── interfaces │ │ ├── storeActions.ts │ │ ├── index.ts │ │ ├── createActions.ts │ │ ├── updateActions.ts │ │ ├── crudActions.ts │ │ ├── deleteActions.ts │ │ └── readActions.ts │ ├── index.ts │ ├── actionsCreatorFactory.ts │ ├── createActionsCreatorFactory.ts │ ├── updateActionsCreatorFactory.ts │ ├── deleteActionsCreatorFactory.ts │ ├── readActionsCreatorFactory.ts │ └── __tests__ │ │ ├── createActionsCreatorFactory-test.ts │ │ ├── updateActionsCreatorFactory-test.ts │ │ ├── actionsCreatorFactory-test.ts │ │ ├── deleteActionsCreatorFactory-test.ts │ │ └── readActionsCreatorFactory-test.ts ├── index.ts └── __tests__ │ └── reduxCrudObservable-test.ts ├── id_rsa.enc ├── .gitignore ├── tsconfig.json ├── LICENCE ├── .travis.yml ├── package.json ├── tslint.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 8.9 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage 2 | -------------------------------------------------------------------------------- /src/configManager/index.ts: -------------------------------------------------------------------------------- 1 | export * from './configManager'; 2 | -------------------------------------------------------------------------------- /src/errors/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ApiError'; 2 | -------------------------------------------------------------------------------- /src/crudEntity/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './entities'; 2 | -------------------------------------------------------------------------------- /src/epicFactory/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './epicFactory'; 2 | -------------------------------------------------------------------------------- /src/configManager/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './configManager'; 2 | -------------------------------------------------------------------------------- /src/constantFactory/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ICrudAction'; 2 | -------------------------------------------------------------------------------- /src/selectorFactory/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './selectorFactory'; 2 | -------------------------------------------------------------------------------- /id_rsa.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hourliert/redux-crud-observable/HEAD/id_rsa.enc -------------------------------------------------------------------------------- /src/crudEntity/index.ts: -------------------------------------------------------------------------------- 1 | export * from './formatEntities'; 2 | 3 | export * from './interfaces'; 4 | -------------------------------------------------------------------------------- /src/selectorFactory/index.ts: -------------------------------------------------------------------------------- 1 | export { default as crudSelectorFactory } from './selectorFactory'; 2 | -------------------------------------------------------------------------------- /src/errors/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ApiError } from './ApiError'; 2 | 3 | export * from './interfaces'; 4 | -------------------------------------------------------------------------------- /src/reducerFactory/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './reducerFactory'; 2 | export * from './initialState'; 3 | -------------------------------------------------------------------------------- /src/observableApiConnector/index.ts: -------------------------------------------------------------------------------- 1 | export * from './observableApiConnector'; 2 | 3 | export * from './interfaces'; 4 | -------------------------------------------------------------------------------- /src/epicFactory/index.ts: -------------------------------------------------------------------------------- 1 | export { default as crudEpicFactory } from './epicFactory'; 2 | 3 | export * from './interfaces'; 4 | -------------------------------------------------------------------------------- /src/errors/interfaces/ApiError.ts: -------------------------------------------------------------------------------- 1 | export interface IApiError extends Error { 2 | data?: any; 3 | status?: number; 4 | } 5 | -------------------------------------------------------------------------------- /src/reducerFactory/interfaces/initialState.ts: -------------------------------------------------------------------------------- 1 | export interface InitialState { 2 | bootTime: Date; 3 | value: any; 4 | } 5 | -------------------------------------------------------------------------------- /src/constantFactory/store.ts: -------------------------------------------------------------------------------- 1 | export function INIT_STORE(ENTITY: string): string { 2 | return `INIT_${ENTITY}S_STORE`; 3 | } 4 | -------------------------------------------------------------------------------- /src/reducerFactory/interfaces/reducerFactory.ts: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | 3 | export type CrudState = Map; 4 | -------------------------------------------------------------------------------- /src/configManager/interfaces/configManager.ts: -------------------------------------------------------------------------------- 1 | export interface IConfigManager { 2 | entityKey: string; 3 | memberKey: string; 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | .vscode 4 | 5 | node_modules 6 | ncp-debug.log 7 | npm-debug.log 8 | 9 | coverage 10 | lib 11 | es 12 | doc 13 | .awcache 14 | -------------------------------------------------------------------------------- /src/observableApiConnector/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './observableApiConnector'; 2 | export * from './streamFormatters'; 3 | export * from './requestFormatters'; 4 | -------------------------------------------------------------------------------- /src/actionsCreatorFactory/interfaces/storeActions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'redux'; 2 | 3 | export interface IInitStoreAction extends Action { 4 | payload: { now: Date }; 5 | } 6 | -------------------------------------------------------------------------------- /src/constantFactory/interfaces/ICrudAction.ts: -------------------------------------------------------------------------------- 1 | export interface ICrudAction { 2 | CANCEL: string; 3 | FAIL: string; 4 | FINISH: string; 5 | REQUEST: string; 6 | value: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/epicFactory/interfaces/epicFactory.ts: -------------------------------------------------------------------------------- 1 | import { IApiConfig } from 'observableApiConnector'; 2 | 3 | export interface IEpicParams { 4 | entity: string; 5 | apiConfig: IApiConfig; 6 | } 7 | -------------------------------------------------------------------------------- /src/reducerFactory/index.ts: -------------------------------------------------------------------------------- 1 | export { default as crudReducerFactory } from './reducerFactory'; 2 | export { default as initialState } from './initialState'; 3 | 4 | export * from './interfaces'; 5 | -------------------------------------------------------------------------------- /src/errors/ApiError.ts: -------------------------------------------------------------------------------- 1 | import { IApiError } from './interfaces'; 2 | 3 | export default class ApiError extends Error implements IApiError { 4 | public data: any; 5 | public status: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/constantFactory/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create'; 2 | export * from './read'; 3 | export * from './update'; 4 | export * from './delete'; 5 | export * from './store'; 6 | 7 | export * from './interfaces'; 8 | -------------------------------------------------------------------------------- /src/reducerFactory/initialState.ts: -------------------------------------------------------------------------------- 1 | import { InitialState } from './interfaces'; 2 | 3 | const initialState: InitialState = { 4 | bootTime: new Date(), 5 | value: {}, 6 | }; 7 | 8 | export default initialState; 9 | -------------------------------------------------------------------------------- /src/actionsCreatorFactory/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './storeActions'; 2 | export * from './createActions'; 3 | export * from './readActions'; 4 | export * from './updateActions'; 5 | export * from './deleteActions'; 6 | export * from './crudActions'; 7 | -------------------------------------------------------------------------------- /src/observableApiConnector/interfaces/streamFormatters.ts: -------------------------------------------------------------------------------- 1 | import { Observable, AjaxResponse } from 'rxjs'; 2 | 3 | export interface IFormatAjaxStreamConfig { 4 | isList?: boolean; 5 | } 6 | 7 | export interface IFormatAjaxStreamParams extends Observable {} 8 | -------------------------------------------------------------------------------- /src/epicFactory/computeApiConfig.ts: -------------------------------------------------------------------------------- 1 | import { IApiConfig } from 'observableApiConnector'; 2 | 3 | export default function computeApiConfig(base: IApiConfig, override?: IApiConfig): IApiConfig { 4 | return override ? 5 | Object.assign({}, base, override) : 6 | base; 7 | } 8 | -------------------------------------------------------------------------------- /src/selectorFactory/interfaces/selectorFactory.ts: -------------------------------------------------------------------------------- 1 | import { Selector } from 'reselect'; 2 | import { Map } from 'immutable'; 3 | 4 | export interface ICrudSelectors { 5 | storeEntitiesCountSelector: Selector; 6 | entityBootTimeSelector: Selector; 7 | entitiesValueSelector: Selector>; 8 | } 9 | -------------------------------------------------------------------------------- /src/crudEntity/interfaces/entities.ts: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | 3 | export interface IEntity { 4 | _internalHash: string; 5 | } 6 | 7 | export interface IEntitiesList { 8 | _internalMember: Array; 9 | } 10 | 11 | export type FormattedEntity = Map; 12 | export type FormattedEntities = Map; 13 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './reducerFactory'; 2 | export * from './constantFactory'; 3 | export * from './crudEntity'; 4 | export * from './actionsCreatorFactory'; 5 | export * from './observableApiConnector'; 6 | export * from './selectorFactory'; 7 | export * from './epicFactory'; 8 | export { setEntityKey, setMemberKey, resetConfig } from './configManager'; 9 | -------------------------------------------------------------------------------- /src/constantFactory/__tests__/store-test.ts: -------------------------------------------------------------------------------- 1 | import { INIT_STORE } from '../store'; 2 | 3 | describe('STORE constant factory', () => { 4 | const ENTITY = 'NINJA'; 5 | 6 | it('creates an object containing INIT constants', () => { 7 | const constant = INIT_STORE(ENTITY); 8 | 9 | expect(constant).toEqual('INIT_NINJAS_STORE'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/constantFactory/create.ts: -------------------------------------------------------------------------------- 1 | import { ICrudAction } from './interfaces'; 2 | 3 | export function CREATE(ENTITY: string): ICrudAction { 4 | return { 5 | CANCEL: `CANCEL_CREATE_${ENTITY}`, 6 | FAIL: `FAIL_CREATE_${ENTITY}`, 7 | FINISH: `FINISH_CREATE_${ENTITY}`, 8 | REQUEST: `REQUEST_CREATE_${ENTITY}`, 9 | value: `CREATE_${ENTITY}`, 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/constantFactory/update.ts: -------------------------------------------------------------------------------- 1 | import { ICrudAction } from './interfaces'; 2 | 3 | export function UPDATE(ENTITY: string): ICrudAction { 4 | return { 5 | CANCEL: `CANCEL_UPDATE_${ENTITY}`, 6 | FAIL: `FAIL_UPDATE_${ENTITY}`, 7 | FINISH: `FINISH_UPDATE_${ENTITY}`, 8 | REQUEST: `REQUEST_UPDATE_${ENTITY}`, 9 | value: `UPDATE_${ENTITY}`, 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/errors/__tests__/ApiError-test.ts: -------------------------------------------------------------------------------- 1 | import ApiError from '../ApiError'; 2 | 3 | describe('ApiError', () => { 4 | it('create an ApiError', () => { 5 | const error = new ApiError(); 6 | 7 | error.data = 'Error'; 8 | error.status = 500; 9 | 10 | expect(error).toBeInstanceOf(Error); 11 | expect(error.data).toEqual('Error'); 12 | expect(error.status).toEqual(500); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/observableApiConnector/interfaces/observableApiConnector.ts: -------------------------------------------------------------------------------- 1 | import { IApiConfig } from './requestFormatters'; 2 | 3 | export interface IRequestParams { 4 | queryParams?: Object; 5 | config: IApiConfig; 6 | } 7 | 8 | export interface IAjaxStreamParams extends ICrudActions { 9 | method: string; 10 | responseType?: string; 11 | } 12 | 13 | export interface ICrudActions extends IRequestParams { 14 | id?: number|string; 15 | body?: any; 16 | } 17 | -------------------------------------------------------------------------------- /src/reducerFactory/computeInitialState.ts: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | 3 | import { CrudState, InitialState } from './interfaces'; 4 | 5 | export default function computeInitialState(initialCrudState: InitialState, additionalState?: any): CrudState { 6 | if (!initialCrudState) throw new Error('Missing initialCrudState'); 7 | 8 | const initialImmutableState = fromJS(initialCrudState) 9 | .merge(additionalState); 10 | 11 | return initialImmutableState; 12 | } 13 | -------------------------------------------------------------------------------- /src/actionsCreatorFactory/index.ts: -------------------------------------------------------------------------------- 1 | export { default as crudActionsCreatorFactory } from './actionsCreatorFactory'; 2 | export { default as createCrudActionsCreatorFactory } from './createActionsCreatorFactory'; 3 | export { default as readCrudActionsCreatorFactory } from './readActionsCreatorFactory'; 4 | export { default as updateCrudActionsCreatorFactory } from './updateActionsCreatorFactory'; 5 | export { default as deleteCrudActionsCreatorFactory } from './deleteActionsCreatorFactory'; 6 | 7 | export * from './interfaces'; 8 | -------------------------------------------------------------------------------- /src/reducerFactory/storeHandlersFactory.ts: -------------------------------------------------------------------------------- 1 | import { ReducersMapObject } from 'redux'; 2 | 3 | import { INIT_STORE } from 'constantFactory'; 4 | import { IInitStoreAction } from 'actionsCreatorFactory'; 5 | 6 | import { CrudState } from './interfaces'; 7 | 8 | export default function storeHandlersFactory(ENTITY: string): ReducersMapObject { 9 | return { 10 | [INIT_STORE(ENTITY)](state: CrudState, action: IInitStoreAction): CrudState { 11 | return state.set('bootTime', action.payload.now); 12 | }, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/constantFactory/__tests__/create-test.ts: -------------------------------------------------------------------------------- 1 | import { CREATE } from '../create'; 2 | 3 | describe('CREATE constant factory', () => { 4 | const ENTITY = 'NINJA'; 5 | 6 | it('creates an object containing CREATE constants', () => { 7 | const constants = CREATE(ENTITY); 8 | 9 | expect(constants).toEqual({ 10 | CANCEL: `CANCEL_CREATE_NINJA`, 11 | FAIL: `FAIL_CREATE_NINJA`, 12 | FINISH: `FINISH_CREATE_NINJA`, 13 | REQUEST: `REQUEST_CREATE_NINJA`, 14 | value: `CREATE_NINJA`, 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/constantFactory/__tests__/update-test.ts: -------------------------------------------------------------------------------- 1 | import { UPDATE } from '../update'; 2 | 3 | describe('UPDATE constant factory', () => { 4 | const ENTITY = 'NINJA'; 5 | 6 | it('creates an object containing UPDATE constants', () => { 7 | const constants = UPDATE(ENTITY); 8 | 9 | expect(constants).toEqual({ 10 | CANCEL: `CANCEL_UPDATE_NINJA`, 11 | FAIL: `FAIL_UPDATE_NINJA`, 12 | FINISH: `FINISH_UPDATE_NINJA`, 13 | REQUEST: `REQUEST_UPDATE_NINJA`, 14 | value: `UPDATE_NINJA`, 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/configManager/configManager.ts: -------------------------------------------------------------------------------- 1 | import { IConfigManager } from './interfaces'; 2 | 3 | class ConfigManager implements IConfigManager { 4 | public entityKey: string = 'hash'; 5 | public memberKey: string = 'member'; 6 | } 7 | 8 | export let configManager: IConfigManager = new ConfigManager(); 9 | export function setEntityKey(key: string): void { 10 | configManager.entityKey = key; 11 | } 12 | 13 | export function setMemberKey(key: string): void { 14 | configManager.memberKey = key; 15 | } 16 | 17 | export function resetConfig(): void { 18 | configManager = new ConfigManager(); 19 | } 20 | -------------------------------------------------------------------------------- /src/epicFactory/epicFactory.ts: -------------------------------------------------------------------------------- 1 | import { combineEpics, Epic } from 'redux-observable'; 2 | 3 | import createEpicFactory from './createEpicFactory'; 4 | import readEpicFactory from './readEpicFactory'; 5 | import updateEpicFactory from './updateEpicFactory'; 6 | import deleteEpicFactory from './deleteEpicFactory'; 7 | 8 | import { IEpicParams } from './interfaces'; 9 | 10 | export default function crudEpicFactory(params: IEpicParams): Epic { 11 | return combineEpics( 12 | createEpicFactory(params), 13 | readEpicFactory(params), 14 | updateEpicFactory(params), 15 | deleteEpicFactory(params), 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/observableApiConnector/interfaces/requestFormatters.ts: -------------------------------------------------------------------------------- 1 | export type ApiProto = 'http' | 'https'; 2 | 3 | export interface IApiUrlParams { 4 | apiProto: ApiProto; 5 | baseUrl: string; 6 | version: string; 7 | route: string; 8 | } 9 | 10 | export interface IParametrizedApiUrlParams extends IApiUrlParams { 11 | id?: number|string; 12 | queryParams?: Object; 13 | } 14 | 15 | export interface IHeadersParams { 16 | token?: string; 17 | json?: boolean; 18 | } 19 | 20 | export interface IHeadersMap { 21 | [index: string]: string; 22 | } 23 | 24 | export interface IApiConfig extends IApiUrlParams, IHeadersParams {} 25 | 26 | -------------------------------------------------------------------------------- /src/reducerFactory/createHandlersFactory.ts: -------------------------------------------------------------------------------- 1 | import { ReducersMapObject } from 'redux'; 2 | 3 | import { CREATE } from 'constantFactory'; 4 | import { formatEntity } from 'crudEntity'; 5 | import { IFinishCreateEntityAction } from 'actionsCreatorFactory'; 6 | 7 | import { CrudState } from './interfaces'; 8 | 9 | export default function createHandlersFactory(ENTITY: string): ReducersMapObject { 10 | return { 11 | [CREATE(ENTITY).FINISH](state: CrudState, action: IFinishCreateEntityAction): CrudState { 12 | if (!action.payload) return state; 13 | 14 | return state 15 | .mergeIn(['value'], formatEntity(action.payload)); 16 | }, 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/reducerFactory/updateHandlersFactory.ts: -------------------------------------------------------------------------------- 1 | import { ReducersMapObject } from 'redux'; 2 | 3 | import { UPDATE } from 'constantFactory'; 4 | import { formatEntity } from 'crudEntity'; 5 | import { IFinishUpdateEntityAction } from 'actionsCreatorFactory'; 6 | 7 | import { CrudState } from './interfaces'; 8 | 9 | export default function updateHandlersFactory(ENTITY: string): ReducersMapObject { 10 | return { 11 | [UPDATE(ENTITY).FINISH](state: CrudState, action: IFinishUpdateEntityAction): CrudState { 12 | if (!action.payload) return state; 13 | 14 | return state 15 | .mergeIn(['value'], formatEntity(action.payload)); 16 | }, 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/constantFactory/delete.ts: -------------------------------------------------------------------------------- 1 | import { ICrudAction } from './interfaces'; 2 | 3 | export function DELETE(ENTITY: string): ICrudAction { 4 | return { 5 | CANCEL: `CANCEL_DELETE_${ENTITY}`, 6 | FAIL: `FAIL_DELETE_${ENTITY}`, 7 | FINISH: `FINISH_DELETE_${ENTITY}`, 8 | REQUEST: `REQUEST_DELETE_${ENTITY}`, 9 | value: `DELETE_${ENTITY}`, 10 | }; 11 | } 12 | 13 | export function DELETE_BATCH(ENTITY: string): ICrudAction { 14 | return { 15 | CANCEL: `CANCEL_DELETE_BATCH_${ENTITY}S`, 16 | FAIL: `FAIL_DELETE_BATCH_${ENTITY}S`, 17 | FINISH: `FINISH_DELETE_BATCH_${ENTITY}S`, 18 | REQUEST: `REQUEST_DELETE_BATCH_${ENTITY}S`, 19 | value: `DELETE_BATCH_${ENTITY}S`, 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/actionsCreatorFactory/actionsCreatorFactory.ts: -------------------------------------------------------------------------------- 1 | import createActionsCreatorFactory from './createActionsCreatorFactory'; 2 | import readActionsCreatorFactory from './readActionsCreatorFactory'; 3 | import updateActionsCreatorFactory from './updateActionsCreatorFactory'; 4 | import deleteActionsCreatorFactory from './deleteActionsCreatorFactory'; 5 | 6 | import { ICrudActionsCreators } from './interfaces'; 7 | 8 | export default function crudActionsCreatorsFactory(ENTITY: string): ICrudActionsCreators { 9 | return { 10 | ...createActionsCreatorFactory(ENTITY), 11 | ...readActionsCreatorFactory(ENTITY), 12 | ...updateActionsCreatorFactory(ENTITY), 13 | ...deleteActionsCreatorFactory(ENTITY), 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/actionsCreatorFactory/createActionsCreatorFactory.ts: -------------------------------------------------------------------------------- 1 | import { actionsCreatorFactory } from 'redux-rac-utils'; 2 | import { CREATE } from 'constantFactory'; 3 | import { IEntity } from 'crudEntity'; 4 | 5 | import { 6 | IRequestCreateEntityPayload, 7 | ICreateActionsCreators, 8 | } from './interfaces'; 9 | 10 | export default function createActionsCreatorsFactory(ENTITY: string): ICreateActionsCreators { 11 | return { 12 | cancelCreateEntity: actionsCreatorFactory(CREATE(ENTITY).CANCEL), 13 | failCreateEntity: actionsCreatorFactory(CREATE(ENTITY).FAIL), 14 | finishCreateEntity: actionsCreatorFactory(CREATE(ENTITY).FINISH), 15 | requestCreateEntity: actionsCreatorFactory(CREATE(ENTITY).REQUEST), 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/actionsCreatorFactory/updateActionsCreatorFactory.ts: -------------------------------------------------------------------------------- 1 | import { actionsCreatorFactory } from 'redux-rac-utils'; 2 | import { UPDATE } from 'constantFactory'; 3 | import { IEntity } from 'crudEntity'; 4 | 5 | import { 6 | IRequestUpdateEntityPayload, 7 | IUpdateActionsCreators, 8 | } from './interfaces'; 9 | 10 | export default function updateActionsCreatorsFactory(ENTITY: string): IUpdateActionsCreators { 11 | return { 12 | cancelUpdateEntity: actionsCreatorFactory(UPDATE(ENTITY).CANCEL), 13 | failUpdateEntity: actionsCreatorFactory(UPDATE(ENTITY).FAIL), 14 | finishUpdateEntity: actionsCreatorFactory(UPDATE(ENTITY).FINISH), 15 | requestUpdateEntity: actionsCreatorFactory(UPDATE(ENTITY).REQUEST), 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/configManager/__tests__/configManager-test.ts: -------------------------------------------------------------------------------- 1 | import { configManager, setEntityKey, setMemberKey, resetConfig } from '../configManager'; 2 | 3 | describe('Config Manager', () => { 4 | it('gets the config manager', () => { 5 | expect(configManager).toBeDefined(); 6 | }); 7 | 8 | it('sets the entity key in the config manager', () => { 9 | setEntityKey('test'); 10 | expect(configManager.entityKey).toEqual('test'); 11 | }); 12 | 13 | it('sets the member key in the config manager', () => { 14 | setMemberKey('test'); 15 | expect(configManager.memberKey).toEqual('test'); 16 | }); 17 | 18 | it('sets the member key in the config manager', () => { 19 | setMemberKey('test'); 20 | resetConfig(); 21 | expect(configManager.memberKey).toEqual('member'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/epicFactory/__tests__/computeApiConfig-test.ts: -------------------------------------------------------------------------------- 1 | import { IApiConfig } from 'observableApiConnector'; 2 | 3 | import computeApiConfig from '../computeApiConfig'; 4 | 5 | describe('computeApiConfig', () => { 6 | const apiConfig: IApiConfig = { 7 | apiProto: 'https', 8 | baseUrl: 'api.starwars.galaxy', 9 | route: '/jedis', 10 | version: '/v1', 11 | }; 12 | 13 | it('creates a new config', () => { 14 | const config = computeApiConfig(apiConfig, { 15 | apiProto: 'https', 16 | baseUrl: 'api.starwars.galaxy', 17 | route: '/jedi', 18 | version: '/v1', 19 | }); 20 | 21 | expect(config).toEqual({ 22 | apiProto: 'https', 23 | baseUrl: 'api.starwars.galaxy', 24 | route: '/jedi', 25 | version: '/v1', 26 | }); 27 | }); 28 | }); 29 | 30 | 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "target": "es5", 6 | "sourceMap": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "outDir": "lib", 10 | "declaration": true, 11 | "moduleResolution": "node", 12 | "pretty": true, 13 | "stripInternal": true, 14 | "noImplicitAny": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | "strictNullChecks": true, 20 | "baseUrl": "src", 21 | "lib": [ 22 | "dom", 23 | "es2015", 24 | "scripthost" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "lib", 30 | "es", 31 | "coverage" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /src/actionsCreatorFactory/interfaces/createActions.ts: -------------------------------------------------------------------------------- 1 | import { IActionCreator, IAction } from 'redux-rac-utils'; 2 | import { IEntity } from 'crudEntity'; 3 | 4 | import { 5 | IRequestCrudActionPayload, 6 | } from './crudActions'; 7 | 8 | export interface IRequestCreateEntityAction extends IAction {} 9 | 10 | export interface IFinishCreateEntityAction extends IAction {} 11 | 12 | export interface IRequestCreateEntityPayload extends IRequestCrudActionPayload { 13 | body: any; 14 | } 15 | 16 | export interface ICreateActionsCreators { 17 | cancelCreateEntity: IActionCreator; 18 | failCreateEntity: IActionCreator; 19 | finishCreateEntity: IActionCreator; 20 | requestCreateEntity: IActionCreator; 21 | } 22 | -------------------------------------------------------------------------------- /src/actionsCreatorFactory/interfaces/updateActions.ts: -------------------------------------------------------------------------------- 1 | import { IActionCreator, IAction } from 'redux-rac-utils'; 2 | import { IEntity } from 'crudEntity'; 3 | 4 | import { 5 | IRequestCrudActionPayload, 6 | } from './crudActions'; 7 | 8 | export interface IRequestUpdateEntityAction extends IAction {} 9 | 10 | export interface IFinishUpdateEntityAction extends IAction {} 11 | 12 | export interface IRequestUpdateEntityPayload extends IRequestCrudActionPayload { 13 | id: string|number; 14 | body: any; 15 | } 16 | 17 | export interface IUpdateActionsCreators { 18 | cancelUpdateEntity: IActionCreator; 19 | failUpdateEntity: IActionCreator; 20 | finishUpdateEntity: IActionCreator; 21 | requestUpdateEntity: IActionCreator; 22 | } 23 | -------------------------------------------------------------------------------- /src/observableApiConnector/streamFormatters.ts: -------------------------------------------------------------------------------- 1 | import { Observable, AjaxResponse, AjaxError } from 'rxjs'; 2 | import { ApiError, IApiError } from 'errors'; 3 | 4 | import { 5 | IFormatAjaxStreamParams, 6 | } from './interfaces'; 7 | 8 | export function formatResponse(ajaxResponse: AjaxResponse): any { 9 | return ajaxResponse.response; 10 | } 11 | 12 | export function formatError(ajaxError: AjaxError): IApiError { 13 | const error = new ApiError(ajaxError.message); 14 | 15 | if (ajaxError.xhr) { 16 | error.data = ajaxError.xhr.response; 17 | error.status = ajaxError.xhr.status; 18 | } 19 | 20 | return error; 21 | } 22 | 23 | export function formatAjaxStream(stream$: IFormatAjaxStreamParams): Observable { 24 | return stream$ 25 | .map(value => formatResponse(value)) 26 | .catch(error => Observable.throw(formatError(error))); 27 | } 28 | -------------------------------------------------------------------------------- /src/reducerFactory/__tests__/computeInitialState-test.ts: -------------------------------------------------------------------------------- 1 | import initialState from '../initialState'; 2 | 3 | import computeInitialState from '../computeInitialState'; 4 | 5 | describe('Compute Initial Crud State', () => { 6 | it('throws an error if the function has invalid paramters', () => { 7 | expect(() => computeInitialState(undefined)) 8 | .toThrowError('Missing initialCrudState'); 9 | }); 10 | 11 | it('computes the initial crud state', () => { 12 | const computedState = computeInitialState(initialState); 13 | 14 | expect(computedState.get('value').toJS()).toEqual({}); 15 | }); 16 | 17 | it('upgrades the initial state', () => { 18 | const computedState = computeInitialState(initialState, { 19 | myMaster: 'yoda', 20 | }); 21 | 22 | expect(computedState.get('value').toJS()).toEqual({}); 23 | expect(computedState.get('myMaster')).toEqual('yoda'); 24 | }); 25 | }); 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/constantFactory/__tests__/delete-test.ts: -------------------------------------------------------------------------------- 1 | import { DELETE, DELETE_BATCH } from '../delete'; 2 | 3 | describe('DELETE constant factory', () => { 4 | const ENTITY = 'NINJA'; 5 | 6 | it('creates an object containing DELETE constants', () => { 7 | const constants = DELETE(ENTITY); 8 | 9 | expect(constants).toEqual({ 10 | CANCEL: `CANCEL_DELETE_NINJA`, 11 | FAIL: `FAIL_DELETE_NINJA`, 12 | FINISH: `FINISH_DELETE_NINJA`, 13 | REQUEST: `REQUEST_DELETE_NINJA`, 14 | value: `DELETE_NINJA`, 15 | }); 16 | }); 17 | 18 | it('creates an object containing DELETE_BATCH constants', () => { 19 | const constants = DELETE_BATCH(ENTITY); 20 | 21 | expect(constants).toEqual({ 22 | CANCEL: `CANCEL_DELETE_BATCH_NINJAS`, 23 | FAIL: `FAIL_DELETE_BATCH_NINJAS`, 24 | FINISH: `FINISH_DELETE_BATCH_NINJAS`, 25 | REQUEST: `REQUEST_DELETE_BATCH_NINJAS`, 26 | value: `DELETE_BATCH_NINJAS`, 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/actionsCreatorFactory/interfaces/crudActions.ts: -------------------------------------------------------------------------------- 1 | import { IAction } from 'redux-rac-utils'; 2 | import { IApiConfig } from 'observableApiConnector'; 3 | 4 | import { ICreateActionsCreators } from './createActions'; 5 | import { IReadActionsCreators } from './readActions'; 6 | import { IUpdateActionsCreators } from './updateActions'; 7 | import { IDeleteActionsCreators } from './deleteActions'; 8 | 9 | export interface ICrudActionsCreators extends 10 | ICreateActionsCreators, 11 | IReadActionsCreators, 12 | IUpdateActionsCreators, 13 | IDeleteActionsCreators {} 14 | 15 | export interface IRequestCrudAction extends IAction {} 16 | 17 | export interface IRequestCrudActionPayload { 18 | queryParams?: Object; 19 | api?: IApiConfig; 20 | } 21 | 22 | export interface IFinishCrudAction extends IAction {} 23 | 24 | export interface IFailCrudAction extends IAction {} 25 | 26 | export interface ICancelCrudAction extends IAction {} 27 | -------------------------------------------------------------------------------- /src/constantFactory/read.ts: -------------------------------------------------------------------------------- 1 | import { ICrudAction } from './interfaces'; 2 | 3 | export function READ(ENTITY: string): ICrudAction { 4 | return { 5 | CANCEL: `CANCEL_READ_${ENTITY}`, 6 | FAIL: `FAIL_READ_${ENTITY}`, 7 | FINISH: `FINISH_READ_${ENTITY}`, 8 | REQUEST: `REQUEST_READ_${ENTITY}`, 9 | value: `READ_${ENTITY}`, 10 | }; 11 | } 12 | 13 | export function READ_BATCH(ENTITY: string): ICrudAction { 14 | return { 15 | CANCEL: `CANCEL_READ_BATCH_${ENTITY}S`, 16 | FAIL: `FAIL_READ_BATCH_${ENTITY}S`, 17 | FINISH: `FINISH_READ_BATCH_${ENTITY}S`, 18 | REQUEST: `REQUEST_READ_BATCH_${ENTITY}S`, 19 | value: `READ_BATCH_${ENTITY}S`, 20 | }; 21 | } 22 | 23 | export function READ_LIST(ENTITY: string): ICrudAction { 24 | return { 25 | CANCEL: `CANCEL_READ_${ENTITY}S_LIST`, 26 | FAIL: `FAIL_READ_${ENTITY}S_LIST`, 27 | FINISH: `FINISH_READ_${ENTITY}S_LIST`, 28 | REQUEST: `REQUEST_READ_${ENTITY}S_LIST`, 29 | value: `READ_${ENTITY}S_LIST`, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/selectorFactory/selectorFactory.ts: -------------------------------------------------------------------------------- 1 | import { get } from 'lodash'; 2 | 3 | import { CrudState } from 'reducerFactory'; 4 | import { createSelector } from 'reselect'; 5 | 6 | import { ICrudSelectors } from './interfaces'; 7 | 8 | export default function crudSelectorsFactory(storeKeyPath: string[]): ICrudSelectors { 9 | const entitiesStoreSelector = (state: any): CrudState => get(state, storeKeyPath); 10 | 11 | const entitiesValueSelector = createSelector( 12 | entitiesStoreSelector, 13 | (entitiesStore) => entitiesStore.get('value'), 14 | ); 15 | 16 | const entityBootTimeSelector = createSelector( 17 | entitiesStoreSelector, 18 | (entitiesStore) => entitiesStore.get('bootTime'), 19 | ); 20 | 21 | const storeEntitiesCountSelector = createSelector( 22 | entitiesValueSelector, 23 | (entitiesValue) => entitiesValue.size, 24 | ); 25 | 26 | return { 27 | storeEntitiesCountSelector, 28 | entityBootTimeSelector, 29 | entitiesValueSelector, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/reducerFactory/deleteHandlersFactory.ts: -------------------------------------------------------------------------------- 1 | import { ReducersMapObject } from 'redux'; 2 | 3 | import { DELETE, DELETE_BATCH } from 'constantFactory'; 4 | import { IFinishDeleteEntityAction, IFinishDeleteEntitiesBatchAction } from 'actionsCreatorFactory'; 5 | 6 | import { CrudState } from './interfaces'; 7 | 8 | export default function deleteHandlersFactory(ENTITY: string): ReducersMapObject { 9 | return { 10 | [DELETE(ENTITY).FINISH](state: CrudState, action: IFinishDeleteEntityAction): CrudState { 11 | if (!action.payload) return state; 12 | 13 | return state 14 | .deleteIn(['value', action.payload._internalHash]); 15 | }, 16 | 17 | [DELETE_BATCH(ENTITY).FINISH](state: CrudState, action: IFinishDeleteEntitiesBatchAction): CrudState { 18 | if (!action.payload) return state; 19 | 20 | const entitiesIds = action.payload.map(e => e._internalHash); 21 | 22 | return state 23 | .update('value', value => 24 | value.filterNot((_: any, k: string) => entitiesIds.indexOf(k) > -1), 25 | ); 26 | }, 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Thomas Hourlier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/reducerFactory/reducerFactory.ts: -------------------------------------------------------------------------------- 1 | import { Reducer, ReducersMapObject } from 'redux'; 2 | import { reducerFactory } from 'redux-rac-utils'; 3 | 4 | import { CrudState } from './interfaces'; 5 | import initialState from './initialState'; 6 | import computeInitialState from './computeInitialState'; 7 | import storeHandlersFactory from './storeHandlersFactory'; 8 | import createHandlersFactory from './createHandlersFactory'; 9 | import readHandlersFactory from './readHandlersFactory'; 10 | import updateHandlersFactory from './updateHandlersFactory'; 11 | import deleteHandlersFactory from './deleteHandlersFactory'; 12 | 13 | export default function crudReducerFactory( 14 | ENTITY: string, 15 | upgradedState?: any, 16 | upgradedReducers?: ReducersMapObject, 17 | ): Reducer { 18 | if (!ENTITY) throw new Error('ENTITY is missing'); 19 | 20 | return reducerFactory( 21 | computeInitialState( 22 | initialState, 23 | upgradedState, 24 | ), 25 | { 26 | ...storeHandlersFactory(ENTITY), 27 | ...createHandlersFactory(ENTITY), 28 | ...readHandlersFactory(ENTITY), 29 | ...updateHandlersFactory(ENTITY), 30 | ...deleteHandlersFactory(ENTITY), 31 | 32 | ...upgradedReducers, 33 | }, 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/actionsCreatorFactory/deleteActionsCreatorFactory.ts: -------------------------------------------------------------------------------- 1 | import { actionsCreatorFactory } from 'redux-rac-utils'; 2 | import { DELETE, DELETE_BATCH } from 'constantFactory'; 3 | import { IEntity } from 'crudEntity'; 4 | 5 | import { 6 | IRequestDeleteEntityPayload, 7 | IRequestDeleteEntitiesPayload, 8 | IDeleteActionsCreators, 9 | } from './interfaces'; 10 | 11 | export default function deleteActionsCreatorFactory(ENTITY: string): IDeleteActionsCreators { 12 | return { 13 | cancelDeleteBatchEntities: actionsCreatorFactory(DELETE_BATCH(ENTITY).CANCEL), 14 | cancelDeleteEntity: actionsCreatorFactory(DELETE(ENTITY).CANCEL), 15 | 16 | failDeleteBatchEntities: actionsCreatorFactory(DELETE_BATCH(ENTITY).FAIL), 17 | failDeleteEntity: actionsCreatorFactory(DELETE(ENTITY).FAIL), 18 | 19 | finishDeleteBatchEntities: actionsCreatorFactory, any>(DELETE_BATCH(ENTITY).FINISH), 20 | finishDeleteEntity: actionsCreatorFactory(DELETE(ENTITY).FINISH), 21 | 22 | requestDeleteBatchEntities: actionsCreatorFactory(DELETE_BATCH(ENTITY).REQUEST), 23 | requestDeleteEntity: actionsCreatorFactory(DELETE(ENTITY).REQUEST), 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/reducerFactory/readHandlersFactory.ts: -------------------------------------------------------------------------------- 1 | import { ReducersMapObject } from 'redux'; 2 | 3 | import { READ, READ_BATCH, READ_LIST } from 'constantFactory'; 4 | import { formatEntity, formatEntities, formatEntitiesList } from 'crudEntity'; 5 | import { IFinishReadEntityAction, IFinishReadEntitiesBatchAction, IFinishReadEntitiesListAction } from 'actionsCreatorFactory'; 6 | 7 | import { CrudState } from './interfaces'; 8 | 9 | export default function readHandlersFactory(ENTITY: string): ReducersMapObject { 10 | return { 11 | [READ(ENTITY).FINISH](state: CrudState, action: IFinishReadEntityAction): CrudState { 12 | if (!action.payload) return state; 13 | 14 | return state 15 | .mergeIn(['value'], formatEntity(action.payload)); 16 | }, 17 | 18 | [READ_BATCH(ENTITY).FINISH](state: CrudState, action: IFinishReadEntitiesBatchAction): CrudState { 19 | if (!action.payload) return state; 20 | 21 | return state 22 | .mergeIn(['value'], formatEntities(action.payload)); 23 | }, 24 | 25 | [READ_LIST(ENTITY).FINISH](state: CrudState, action: IFinishReadEntitiesListAction): CrudState { 26 | if (!action.payload) return state; 27 | 28 | return state 29 | .mergeIn(['value'], formatEntitiesList(action.payload)); 30 | }, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/observableApiConnector/requestFormatters.ts: -------------------------------------------------------------------------------- 1 | import * as qs from 'qs'; 2 | 3 | import { 4 | IApiUrlParams, 5 | IParametrizedApiUrlParams, 6 | IHeadersParams, 7 | IHeadersMap, 8 | } from './interfaces'; 9 | 10 | export function computeCompleteUrl({ apiProto, baseUrl, version, route }: IApiUrlParams = { 11 | apiProto: 'https', 12 | baseUrl: 'www.example.com', 13 | route: '/', 14 | version: '/v1', 15 | }): string { 16 | const uri = `${baseUrl}${version}${route}` 17 | .replace(/(\/)(?=\1)/g, ''); // delete consecutive /; 18 | 19 | return `${apiProto}://${uri}`; 20 | } 21 | 22 | export function computeParametrizedUrl({ 23 | id, 24 | queryParams, 25 | ...apiUrlParams, 26 | }: IParametrizedApiUrlParams): string { 27 | let url = `${ 28 | computeCompleteUrl(apiUrlParams) 29 | }`; 30 | 31 | if (id) { 32 | url = url.concat(`/${id.toString()}`); 33 | } 34 | 35 | if (queryParams) { 36 | url = url.concat(`/?${qs.stringify(queryParams)}`); 37 | } 38 | 39 | url = url.replace(/(\?)*$/g, ''); // delete trailing ? 40 | 41 | return url; 42 | } 43 | 44 | export function computeHeaders(params: IHeadersParams): IHeadersMap { 45 | return { 46 | ...(params.token ? { Authorization: `${params.token}` } : {}), 47 | ...(params.json ? { 'Content-Type': 'application/json' } : {}), 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /src/crudEntity/formatEntities.ts: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | 3 | import { configManager } from 'configManager'; 4 | 5 | import { IEntity, IEntitiesList, FormattedEntity, FormattedEntities } from './interfaces'; 6 | 7 | export function formatEntity(entity: IEntity): FormattedEntity { 8 | return fromJS({ 9 | [entity._internalHash]: entity, 10 | }); 11 | } 12 | 13 | export function formatEntities(entities: Array): FormattedEntities { 14 | return entities.reduce((res: FormattedEntity, cur: IEntity) => ( 15 | res.merge(formatEntity(cur)) 16 | ), fromJS({})); 17 | } 18 | 19 | export function formatEntitiesList(entitiesList: IEntitiesList): FormattedEntities { 20 | return formatEntities(entitiesList._internalMember); 21 | } 22 | 23 | export function resolveEntityKey(entity: any): IEntity { 24 | return Object.assign(entity, { 25 | _internalHash: entity[configManager.entityKey], 26 | }); 27 | } 28 | 29 | export function resolveEntitiesKey(entities: Array): Array { 30 | return entities.map(e => Object.assign(e, { 31 | _internalHash: e[configManager.entityKey], 32 | })); 33 | } 34 | 35 | export function resolveEntitiesListKey(entitiesList: any): IEntitiesList { 36 | return Object.assign(entitiesList, { 37 | _internalMember: entitiesList[configManager.memberKey], 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /src/constantFactory/__tests__/read-test.ts: -------------------------------------------------------------------------------- 1 | import { READ, READ_BATCH, READ_LIST } from '../read'; 2 | 3 | describe('READ constant factory', () => { 4 | const ENTITY = 'NINJA'; 5 | 6 | it('creates an object containing READ constants', () => { 7 | const constants = READ(ENTITY); 8 | 9 | expect(constants).toEqual({ 10 | CANCEL: `CANCEL_READ_NINJA`, 11 | FAIL: `FAIL_READ_NINJA`, 12 | FINISH: `FINISH_READ_NINJA`, 13 | REQUEST: `REQUEST_READ_NINJA`, 14 | value: `READ_NINJA`, 15 | }); 16 | }); 17 | 18 | it('creates an object containing READ_BATCH constants', () => { 19 | const constants = READ_BATCH(ENTITY); 20 | 21 | expect(constants).toEqual({ 22 | CANCEL: `CANCEL_READ_BATCH_NINJAS`, 23 | FAIL: `FAIL_READ_BATCH_NINJAS`, 24 | FINISH: `FINISH_READ_BATCH_NINJAS`, 25 | REQUEST: `REQUEST_READ_BATCH_NINJAS`, 26 | value: `READ_BATCH_NINJAS`, 27 | }); 28 | }); 29 | 30 | it('creates an object containing READ_LIST constants', () => { 31 | const constants = READ_LIST(ENTITY); 32 | 33 | expect(constants).toEqual({ 34 | CANCEL: `CANCEL_READ_NINJAS_LIST`, 35 | FAIL: `FAIL_READ_NINJAS_LIST`, 36 | FINISH: `FINISH_READ_NINJAS_LIST`, 37 | REQUEST: `REQUEST_READ_NINJAS_LIST`, 38 | value: `READ_NINJAS_LIST`, 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/actionsCreatorFactory/interfaces/deleteActions.ts: -------------------------------------------------------------------------------- 1 | import { IActionCreator, IAction } from 'redux-rac-utils'; 2 | import { IEntity } from 'crudEntity'; 3 | 4 | import { 5 | IRequestCrudActionPayload, 6 | } from './crudActions'; 7 | 8 | export interface IRequestDeleteEntityAction extends IAction {} 9 | export interface IRequestDeleteEntitiesBatchAction extends IAction {} 10 | 11 | export interface IFinishDeleteEntityAction extends IAction {} 12 | export interface IFinishDeleteEntitiesBatchAction extends IAction, any> {} 13 | 14 | export interface IRequestDeleteEntityPayload extends IRequestCrudActionPayload { 15 | id: string|number; 16 | } 17 | 18 | export interface IRequestDeleteEntitiesPayload extends IRequestCrudActionPayload { 19 | ids: Array; 20 | } 21 | 22 | export interface IDeleteActionsCreators { 23 | cancelDeleteEntity: IActionCreator; 24 | failDeleteEntity: IActionCreator; 25 | finishDeleteEntity: IActionCreator; 26 | requestDeleteEntity: IActionCreator; 27 | 28 | cancelDeleteBatchEntities: IActionCreator; 29 | failDeleteBatchEntities: IActionCreator; 30 | finishDeleteBatchEntities: IActionCreator, any>; 31 | requestDeleteBatchEntities: IActionCreator; 32 | } 33 | -------------------------------------------------------------------------------- /src/selectorFactory/__tests__/selectorFactory-test.ts: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | 3 | import selectorFactory from '../selectorFactory'; 4 | 5 | describe('Selector Factory', () => { 6 | it('creates the selectors', () => { 7 | const selectors = selectorFactory([]); 8 | 9 | expect(selectors).toBeDefined(); 10 | }); 11 | 12 | it('retrieves the store bootTime', () => { 13 | const { entityBootTimeSelector } = selectorFactory(['store']); 14 | 15 | const now = new Date(); 16 | const state = { 17 | store: Map({ 18 | bootTime: now, 19 | }), 20 | }; 21 | 22 | expect(entityBootTimeSelector(state)).toEqual(now); 23 | }); 24 | 25 | it('retrieves the store value', () => { 26 | const { entitiesValueSelector } = selectorFactory(['store']); 27 | 28 | const value = Map({ 29 | 1: { name: 'Yoda' }, 30 | 2: { name: 'Obi Wan' }, 31 | }); 32 | 33 | const state = { 34 | store: Map({ 35 | value, 36 | }), 37 | }; 38 | 39 | expect(entitiesValueSelector(state)).toEqual(value); 40 | }); 41 | 42 | it('retrieves the store value size', () => { 43 | const { storeEntitiesCountSelector } = selectorFactory(['store']); 44 | 45 | const value = Map({ 46 | 1: { name: 'Yoda' }, 47 | 2: { name: 'Obi Wan' }, 48 | }); 49 | 50 | const state = { 51 | store: Map({ 52 | value, 53 | }), 54 | }; 55 | 56 | expect(storeEntitiesCountSelector(state)).toEqual(2); 57 | }); 58 | }); 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - v8 4 | before_install: 5 | - openssl aes-256-cbc -K $encrypted_8469635b2593_key -iv $encrypted_8469635b2593_iv 6 | -in id_rsa.enc -out id_rsa -d 7 | - npm i -g yarn 8 | - yarn global add codecov 9 | after_success: 10 | - rm -rf ./coverage/clover.xml ./coverage/coverage-final.json ./coverage/lcov.info 11 | ./coverage/lcov-report 12 | - codecov -e TRAVIS_NODE_VERSION 13 | - yarn doc 14 | - touch doc/.nojekyll 15 | - gh-pages-travis 16 | deploy: 17 | skip_cleanup: true 18 | provider: npm 19 | email: hourliert@gmail.com 20 | api_key: 21 | secure: pr+ZbM/k2uq6FXuiRZhldDdqcnL76nveG1vic2zueV3DVYIHOrD3Xn48q0noqqyl5HRljSEdfoGXnMerBzSCcoaVDmTrCYli/kKW2/BRkk7k8J8M/1Qcfif7bdKn7xI+fKjb4QTV89EJz/qatIx4+UFHtaBfmP2swvyvYtx754D08/kynQfAVbZvSmEtVYGA+5s7D+8c8Mz82KLDGNFEeCzIFy6ySoLUlhwDu+C5MOGhEg9o6+Lz9v3Q2H4ZlpUwi8uD1f6iUanqKmlo/c/d76c8tLVF/vKaRSrrViSNVVpX2YNwwCjvhvRG2X6AFye8pMZaYj6dJfsieBOMn7FuJFhb9oUsZBpruEAA1coRhzSaOkldgTArAzJvcVcXPpkALtGhpQ7LnEmDd/evwtl3jM3Z3tiWo/vaZfFYhlcHzo6Z4QZ/zY+DFwLetbvG1R71GqircA0/izUsJEV1tDKnybnpFCYkgJIqIFWsoET7det9q+MV9mUPJNB9UQySUav2kiUOstkUnuOyibM11e9VJSPvBP6RgJZR4uKDmfx9av4zIasq9c67GIGpZdg2w5kFr/CTyutR5qdgOdlN58NUbk+eWpLm0CVYQGbaUkxi2kIhcyh8N3trX+EleewHSlfr0xOjPqSjWdtOj3+DRGWnd7U107W2Mt0y9XB2GoMgBxU= 22 | on: 23 | tags: true 24 | repo: FoodMeUp/redux-crud-observable 25 | branches: 26 | except: 27 | - gh-pages 28 | env: 29 | global: 30 | - DEPLOY_BRANCH="master" 31 | - SOURCE_DIR="doc" 32 | - TARGET_BRANCH="gh-pages" 33 | - SSH_KEY="id_rsa" 34 | - GIT_NAME="travis" 35 | - GIT_EMAIL="deploy@travis-ci.org" 36 | cache: 37 | yarn: true 38 | -------------------------------------------------------------------------------- /src/observableApiConnector/observableApiConnector.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { ajax } from 'rxjs/observable/dom/ajax'; 3 | 4 | import { formatAjaxStream } from './streamFormatters'; 5 | import { computeHeaders, computeParametrizedUrl } from './requestFormatters'; 6 | 7 | import { 8 | ICrudActions, 9 | IAjaxStreamParams, 10 | } from './interfaces'; 11 | 12 | export function createAjaxStream({ 13 | id, 14 | queryParams, 15 | method, 16 | body, 17 | responseType, 18 | config: { token, json, ...apiParams }, 19 | }: IAjaxStreamParams): Observable { 20 | return formatAjaxStream(ajax({ 21 | crossDomain: true, 22 | method, 23 | responseType, 24 | body, 25 | headers: computeHeaders({ token, json }), 26 | url: computeParametrizedUrl({ id, queryParams, ...apiParams }), 27 | })); 28 | } 29 | 30 | export function readEntity(params: ICrudActions): Observable { 31 | return createAjaxStream({ 32 | method: 'GET', 33 | responseType: 'json', 34 | ...params, 35 | }); 36 | } 37 | 38 | export function deleteEntity(params: ICrudActions): Observable { 39 | return createAjaxStream({ 40 | method: 'DELETE', 41 | ...params, 42 | }); 43 | } 44 | 45 | export function updateEntity(params: ICrudActions): Observable { 46 | return createAjaxStream({ 47 | method: 'PUT', 48 | responseType: 'json', 49 | ...params, 50 | }); 51 | } 52 | 53 | export function createEntity(params: ICrudActions): Observable { 54 | return createAjaxStream({ 55 | method: 'POST', 56 | responseType: 'json', 57 | ...params, 58 | }); 59 | } 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/actionsCreatorFactory/readActionsCreatorFactory.ts: -------------------------------------------------------------------------------- 1 | import { actionsCreatorFactory } from 'redux-rac-utils'; 2 | import { READ, READ_BATCH, READ_LIST } from 'constantFactory'; 3 | import { IEntity, IEntitiesList } from 'crudEntity'; 4 | 5 | import { 6 | IRequestReadEntityPayload, 7 | IRequestReadEntitiesListPayload, 8 | IRequestReadEntitiesPayload, 9 | IReadActionsCreators, 10 | } from './interfaces'; 11 | 12 | export default function readActionsCreatorFactory(ENTITY: string): IReadActionsCreators { 13 | return { 14 | cancelReadBatchEntities: actionsCreatorFactory(READ_BATCH(ENTITY).CANCEL), 15 | cancelReadEntitiesList: actionsCreatorFactory(READ_LIST(ENTITY).CANCEL), 16 | cancelReadEntity: actionsCreatorFactory(READ(ENTITY).CANCEL), 17 | 18 | failReadBatchEntities: actionsCreatorFactory(READ_BATCH(ENTITY).FAIL), 19 | failReadEntitiesList: actionsCreatorFactory(READ_LIST(ENTITY).FAIL), 20 | failReadEntity: actionsCreatorFactory(READ(ENTITY).FAIL), 21 | 22 | finishReadBatchEntities: actionsCreatorFactory, any>(READ_BATCH(ENTITY).FINISH), 23 | finishReadEntitiesList: actionsCreatorFactory(READ_LIST(ENTITY).FINISH), 24 | finishReadEntity: actionsCreatorFactory(READ(ENTITY).FINISH), 25 | 26 | requestReadBatchEntities: actionsCreatorFactory(READ_BATCH(ENTITY).REQUEST), 27 | requestReadEntitiesList: actionsCreatorFactory(READ_LIST(ENTITY).REQUEST), 28 | requestReadEntity: actionsCreatorFactory(READ(ENTITY).REQUEST), 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/epicFactory/createEpicFactory.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { ActionsObservable, Epic, combineEpics } from 'redux-observable'; 3 | 4 | import { 5 | createEntity, 6 | } from 'observableApiConnector'; 7 | import { IEntity, resolveEntityKey } from 'crudEntity'; 8 | import { 9 | createCrudActionsCreatorFactory, 10 | IRequestCreateEntityAction, 11 | } from 'actionsCreatorFactory'; 12 | import { 13 | CREATE, 14 | } from 'constantFactory'; 15 | 16 | import computeApiConfig from './computeApiConfig'; 17 | import { IEpicParams } from './interfaces'; 18 | 19 | export default function createEpicFactory({ 20 | entity, 21 | apiConfig, 22 | }: IEpicParams): Epic { 23 | const { 24 | finishCreateEntity, 25 | failCreateEntity, 26 | } = createCrudActionsCreatorFactory(entity); 27 | 28 | const createEntityEpic: Epic = (action$: ActionsObservable) => ( 29 | action$.ofType(CREATE(entity).REQUEST) 30 | .switchMap(({ meta, payload }: IRequestCreateEntityAction) => { 31 | if (!payload) return Observable.empty(); 32 | 33 | const config = computeApiConfig(apiConfig, payload.api); 34 | 35 | return createEntity({ 36 | body: payload.body, 37 | config: config, 38 | queryParams: payload.queryParams, 39 | }) 40 | .map((res: any) => resolveEntityKey(res)) 41 | .map((res: IEntity) => finishCreateEntity(res, meta)) 42 | .takeUntil(action$.ofType(CREATE(entity).CANCEL)) 43 | .catch((error: Error) => Observable.of(failCreateEntity(error, meta))); 44 | }) 45 | ); 46 | 47 | return combineEpics( 48 | createEntityEpic, 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/epicFactory/updateEpicFactory.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { ActionsObservable, Epic, combineEpics } from 'redux-observable'; 3 | 4 | import { 5 | updateEntity, 6 | } from 'observableApiConnector'; 7 | import { IEntity, resolveEntityKey } from 'crudEntity'; 8 | import { 9 | updateCrudActionsCreatorFactory, 10 | IRequestUpdateEntityAction, 11 | } from 'actionsCreatorFactory'; 12 | import { 13 | UPDATE, 14 | } from 'constantFactory'; 15 | 16 | import computeApiConfig from './computeApiConfig'; 17 | import { IEpicParams } from './interfaces'; 18 | 19 | export default function updateEpicFactory({ 20 | entity, 21 | apiConfig, 22 | }: IEpicParams): Epic { 23 | const { 24 | finishUpdateEntity, 25 | failUpdateEntity, 26 | } = updateCrudActionsCreatorFactory(entity); 27 | 28 | const updateEntityEpic: Epic = (action$: ActionsObservable) => ( 29 | action$.ofType(UPDATE(entity).REQUEST) 30 | .switchMap(({ meta, payload }: IRequestUpdateEntityAction) => { 31 | if (!payload) return Observable.empty(); 32 | 33 | const config = computeApiConfig(apiConfig, payload.api); 34 | 35 | return updateEntity({ 36 | body: payload.body, 37 | config: config, 38 | id: payload.id, 39 | queryParams: payload.queryParams, 40 | }) 41 | .map((res: any) => resolveEntityKey(res)) 42 | .map((res: IEntity) => finishUpdateEntity(res, meta)) 43 | .takeUntil(action$.ofType(UPDATE(entity).CANCEL)) 44 | .catch((error: Error) => Observable.of(failUpdateEntity(error, meta))); 45 | }) 46 | ); 47 | 48 | return combineEpics( 49 | updateEntityEpic, 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/crudEntity/__tests__/formatEntities-test.ts: -------------------------------------------------------------------------------- 1 | import { formatEntity, formatEntities, formatEntitiesList } from '../formatEntities'; 2 | 3 | describe('Crud Entities', () => { 4 | it('formats a crud entity', () => { 5 | const entity = { 6 | _internalHash: '1234', 7 | name: 'Yoda', 8 | }; 9 | 10 | const formattedEntity = formatEntity(entity); 11 | expect(formattedEntity.toJS()).toEqual({ 12 | 1234: { 13 | _internalHash: '1234', 14 | name: 'Yoda', 15 | }, 16 | }); 17 | }); 18 | 19 | it('formats an array of crud entity', () => { 20 | const entity1 = { 21 | _internalHash: '1234', 22 | name: 'Yoda', 23 | }; 24 | const entity2 = { 25 | _internalHash: '5678', 26 | name: 'Obi Wan', 27 | }; 28 | 29 | const formattedEntities = formatEntities([entity1, entity2]); 30 | expect(formattedEntities.toJS()).toEqual({ 31 | 1234: { 32 | _internalHash: '1234', 33 | name: 'Yoda', 34 | }, 35 | 5678: { 36 | _internalHash: '5678', 37 | name: 'Obi Wan', 38 | }, 39 | }); 40 | }); 41 | 42 | it('formats a list of crud entity', () => { 43 | const entity1 = { 44 | _internalHash: '1234', 45 | name: 'Yoda', 46 | }; 47 | const entity2 = { 48 | _internalHash: '5678', 49 | name: 'Obi Wan', 50 | }; 51 | 52 | const formattedEntities = formatEntitiesList({ 53 | _internalMember: [entity1, entity2], 54 | }); 55 | expect(formattedEntities.toJS()).toEqual({ 56 | 1234: { 57 | _internalHash: '1234', 58 | name: 'Yoda', 59 | }, 60 | 5678: { 61 | _internalHash: '5678', 62 | name: 'Obi Wan', 63 | }, 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/actionsCreatorFactory/interfaces/readActions.ts: -------------------------------------------------------------------------------- 1 | import { IActionCreator, IAction } from 'redux-rac-utils'; 2 | import { IEntity, IEntitiesList } from 'crudEntity'; 3 | 4 | import { 5 | IRequestCrudActionPayload, 6 | } from './crudActions'; 7 | 8 | export interface IRequestReadEntityAction extends IAction {} 9 | export interface IRequestReadEntitiesAction extends IAction {} 10 | export interface IRequestReadEntitiesListAction extends IAction {} 11 | 12 | export interface IFinishReadEntityAction extends IAction {} 13 | export interface IFinishReadEntitiesBatchAction extends IAction, any> {} 14 | export interface IFinishReadEntitiesListAction extends IAction {} 15 | 16 | export interface IRequestReadEntityPayload extends IRequestCrudActionPayload { 17 | id: string|number; 18 | } 19 | 20 | export interface IRequestReadEntitiesPayload extends IRequestCrudActionPayload { 21 | ids: Array; 22 | } 23 | 24 | export interface IRequestReadEntitiesListPayload extends IRequestCrudActionPayload {} 25 | 26 | 27 | export interface IReadActionsCreators { 28 | cancelReadEntity: IActionCreator; 29 | failReadEntity: IActionCreator; 30 | finishReadEntity: IActionCreator; 31 | requestReadEntity: IActionCreator; 32 | 33 | cancelReadEntitiesList: IActionCreator; 34 | failReadEntitiesList: IActionCreator; 35 | finishReadEntitiesList: IActionCreator; 36 | requestReadEntitiesList: IActionCreator; 37 | 38 | cancelReadBatchEntities: IActionCreator; 39 | failReadBatchEntities: IActionCreator; 40 | finishReadBatchEntities: IActionCreator, any>; 41 | requestReadBatchEntities: IActionCreator; 42 | } 43 | -------------------------------------------------------------------------------- /src/actionsCreatorFactory/__tests__/createActionsCreatorFactory-test.ts: -------------------------------------------------------------------------------- 1 | import createActionsCreatorFactory from '../createActionsCreatorFactory'; 2 | 3 | describe('createActionsCreatorFactory', () => { 4 | const ENTITY = 'JEDI'; 5 | 6 | it('creates actions creators', () => { 7 | const actionsCreators = createActionsCreatorFactory(ENTITY); 8 | 9 | expect(actionsCreators).toBeDefined(); 10 | expect(actionsCreators.cancelCreateEntity).toBeInstanceOf(Function); 11 | expect(actionsCreators.failCreateEntity).toBeInstanceOf(Function); 12 | expect(actionsCreators.finishCreateEntity).toBeInstanceOf(Function); 13 | expect(actionsCreators.requestCreateEntity).toBeInstanceOf(Function); 14 | }); 15 | 16 | it('creates a cancel create entity actions creator', () => { 17 | const { cancelCreateEntity } = createActionsCreatorFactory(ENTITY); 18 | 19 | expect(cancelCreateEntity()).toEqual({ 20 | type: 'CANCEL_CREATE_JEDI', 21 | }); 22 | }); 23 | 24 | it('creates a fail create entity actions creator', () => { 25 | const { failCreateEntity } = createActionsCreatorFactory(ENTITY); 26 | const payload = new Error(`Can't create this jedi`); 27 | 28 | expect(failCreateEntity(payload)).toEqual({ 29 | error: true, 30 | payload, 31 | type: 'FAIL_CREATE_JEDI', 32 | }); 33 | }); 34 | 35 | it('creates a finish create entity actions creator', () => { 36 | const { finishCreateEntity } = createActionsCreatorFactory(ENTITY); 37 | const payload = { 38 | hash: '1234', 39 | name: 'Yoda', 40 | }; 41 | 42 | expect(finishCreateEntity(payload)).toEqual({ 43 | payload, 44 | type: 'FINISH_CREATE_JEDI', 45 | }); 46 | }); 47 | 48 | it('creates a cancel create entity actions creator', () => { 49 | const { requestCreateEntity } = createActionsCreatorFactory(ENTITY); 50 | const payload = { 51 | hash: '1234', 52 | }; 53 | 54 | expect(requestCreateEntity(payload)).toEqual({ 55 | payload, 56 | type: 'REQUEST_CREATE_JEDI', 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/actionsCreatorFactory/__tests__/updateActionsCreatorFactory-test.ts: -------------------------------------------------------------------------------- 1 | import updateActionsCreatorFactory from '../updateActionsCreatorFactory'; 2 | 3 | describe('updateActionsCreatorFactory', () => { 4 | const ENTITY = 'JEDI'; 5 | 6 | it('creates actions creators', () => { 7 | const actionsCreators = updateActionsCreatorFactory(ENTITY); 8 | 9 | expect(actionsCreators).toBeDefined(); 10 | expect(actionsCreators.cancelUpdateEntity).toBeInstanceOf(Function); 11 | expect(actionsCreators.failUpdateEntity).toBeInstanceOf(Function); 12 | expect(actionsCreators.finishUpdateEntity).toBeInstanceOf(Function); 13 | expect(actionsCreators.requestUpdateEntity).toBeInstanceOf(Function); 14 | }); 15 | 16 | it('creates a cancel update entity actions creator', () => { 17 | const { cancelUpdateEntity } = updateActionsCreatorFactory(ENTITY); 18 | 19 | expect(cancelUpdateEntity()).toEqual({ 20 | type: 'CANCEL_UPDATE_JEDI', 21 | }); 22 | }); 23 | 24 | it('creates a fail update entity actions creator', () => { 25 | const { failUpdateEntity } = updateActionsCreatorFactory(ENTITY); 26 | const payload = new Error(`Can't create this jedi`); 27 | 28 | expect(failUpdateEntity(payload)).toEqual({ 29 | error: true, 30 | payload, 31 | type: 'FAIL_UPDATE_JEDI', 32 | }); 33 | }); 34 | 35 | it('creates a finish update entity actions creator', () => { 36 | const { finishUpdateEntity } = updateActionsCreatorFactory(ENTITY); 37 | const payload = { 38 | hash: '1234', 39 | name: 'Yoda', 40 | }; 41 | 42 | expect(finishUpdateEntity(payload)).toEqual({ 43 | payload, 44 | type: 'FINISH_UPDATE_JEDI', 45 | }); 46 | }); 47 | 48 | it('creates a cancel update entity actions creator', () => { 49 | const { requestUpdateEntity } = updateActionsCreatorFactory(ENTITY); 50 | const payload = { 51 | hash: '1234', 52 | }; 53 | 54 | expect(requestUpdateEntity(payload)).toEqual({ 55 | payload, 56 | type: 'REQUEST_UPDATE_JEDI', 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/actionsCreatorFactory/__tests__/actionsCreatorFactory-test.ts: -------------------------------------------------------------------------------- 1 | import actionsCreatorFactory from '../actionsCreatorFactory'; 2 | 3 | describe('actionsCreatorFactory', () => { 4 | const ENTITY = 'JEDI'; 5 | 6 | it('imports all actions creators', () => { 7 | const actionsCreators = actionsCreatorFactory(ENTITY); 8 | 9 | expect(actionsCreators).toBeDefined(); 10 | expect(actionsCreators.cancelUpdateEntity).toBeInstanceOf(Function); 11 | expect(actionsCreators.failUpdateEntity).toBeInstanceOf(Function); 12 | expect(actionsCreators.finishUpdateEntity).toBeInstanceOf(Function); 13 | expect(actionsCreators.requestUpdateEntity).toBeInstanceOf(Function); 14 | 15 | expect(actionsCreators.cancelCreateEntity).toBeInstanceOf(Function); 16 | expect(actionsCreators.failCreateEntity).toBeInstanceOf(Function); 17 | expect(actionsCreators.finishCreateEntity).toBeInstanceOf(Function); 18 | expect(actionsCreators.requestCreateEntity).toBeInstanceOf(Function); 19 | 20 | expect(actionsCreators.cancelDeleteEntity).toBeInstanceOf(Function); 21 | expect(actionsCreators.failDeleteEntity).toBeInstanceOf(Function); 22 | expect(actionsCreators.finishDeleteEntity).toBeInstanceOf(Function); 23 | expect(actionsCreators.requestDeleteEntity).toBeInstanceOf(Function); 24 | 25 | expect(actionsCreators.cancelDeleteBatchEntities).toBeInstanceOf(Function); 26 | expect(actionsCreators.failDeleteBatchEntities).toBeInstanceOf(Function); 27 | expect(actionsCreators.finishDeleteBatchEntities).toBeInstanceOf(Function); 28 | expect(actionsCreators.requestDeleteBatchEntities).toBeInstanceOf(Function); 29 | 30 | expect(actionsCreators.cancelReadEntity).toBeInstanceOf(Function); 31 | expect(actionsCreators.failReadEntity).toBeInstanceOf(Function); 32 | expect(actionsCreators.finishReadEntity).toBeInstanceOf(Function); 33 | expect(actionsCreators.requestReadEntity).toBeInstanceOf(Function); 34 | 35 | expect(actionsCreators.cancelReadEntitiesList).toBeInstanceOf(Function); 36 | expect(actionsCreators.failReadEntitiesList).toBeInstanceOf(Function); 37 | expect(actionsCreators.finishReadEntitiesList).toBeInstanceOf(Function); 38 | expect(actionsCreators.requestReadEntitiesList).toBeInstanceOf(Function); 39 | 40 | expect(actionsCreators.cancelReadBatchEntities).toBeInstanceOf(Function); 41 | expect(actionsCreators.failReadBatchEntities).toBeInstanceOf(Function); 42 | expect(actionsCreators.finishReadBatchEntities).toBeInstanceOf(Function); 43 | expect(actionsCreators.requestReadBatchEntities).toBeInstanceOf(Function); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/epicFactory/deleteEpicFactory.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { ActionsObservable, Epic, combineEpics } from 'redux-observable'; 3 | 4 | import { 5 | deleteEntity, 6 | } from 'observableApiConnector'; 7 | import { 8 | deleteCrudActionsCreatorFactory, 9 | IRequestDeleteEntityAction, 10 | IRequestDeleteEntitiesBatchAction, 11 | } from 'actionsCreatorFactory'; 12 | import { 13 | DELETE, 14 | DELETE_BATCH, 15 | } from 'constantFactory'; 16 | 17 | import computeApiConfig from './computeApiConfig'; 18 | import { IEpicParams } from './interfaces'; 19 | 20 | export default function deleteEpicFactory({ 21 | entity, 22 | apiConfig, 23 | }: IEpicParams): Epic { 24 | const { 25 | finishDeleteBatchEntities, 26 | finishDeleteEntity, 27 | 28 | failDeleteBatchEntities, 29 | failDeleteEntity, 30 | } = deleteCrudActionsCreatorFactory(entity); 31 | 32 | const deleteEntityEpic: Epic = (action$: ActionsObservable) => ( 33 | action$.ofType(DELETE(entity).REQUEST) 34 | .switchMap(({ meta, payload }: IRequestDeleteEntityAction) => { 35 | if (!payload) return Observable.empty(); 36 | 37 | const config = computeApiConfig(apiConfig, payload.api); 38 | 39 | return deleteEntity({ 40 | config: config, 41 | id: payload.id, 42 | queryParams: payload.queryParams, 43 | }) 44 | .map(() => finishDeleteEntity(payload.id, meta)) 45 | .takeUntil(action$.ofType(DELETE(entity).CANCEL)) 46 | .catch((error: Error) => Observable.of(failDeleteEntity(error, meta))); 47 | }) 48 | ); 49 | 50 | const deleteBatchEntitiesEpic: Epic = (action$: ActionsObservable) => ( 51 | action$.ofType(DELETE_BATCH(entity).REQUEST) 52 | .switchMap(({ meta, payload }: IRequestDeleteEntitiesBatchAction) => { 53 | if (!payload) return Observable.empty(); 54 | 55 | const config = computeApiConfig(apiConfig, payload.api); 56 | 57 | return Observable 58 | .forkJoin( 59 | payload.ids.map(id => deleteEntity({ 60 | config: config, 61 | id, 62 | queryParams: payload.queryParams, 63 | })), 64 | ) 65 | .map(() => finishDeleteBatchEntities(payload.ids, meta)) 66 | .takeUntil(action$.ofType(DELETE_BATCH(entity).CANCEL)) 67 | .catch((error: Error) => Observable.of(failDeleteBatchEntities(error, meta))); 68 | }) 69 | ); 70 | 71 | return combineEpics( 72 | deleteEntityEpic, 73 | deleteBatchEntitiesEpic, 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /src/observableApiConnector/__tests__/requestFormatters-test.ts: -------------------------------------------------------------------------------- 1 | import { computeCompleteUrl, computeParametrizedUrl, computeHeaders } from '../requestFormatters'; 2 | 3 | import { IApiUrlParams, IParametrizedApiUrlParams } from '../interfaces'; 4 | 5 | describe('requestFormatters', () => { 6 | it('computes a complete url', () => { 7 | const params: IApiUrlParams = { 8 | apiProto: 'https', 9 | baseUrl: 'api.starwars.galaxy', 10 | route: '/jedis', 11 | version: '/v1', 12 | }; 13 | 14 | expect(computeCompleteUrl(params)).toEqual('https://api.starwars.galaxy/v1/jedis'); 15 | }); 16 | 17 | it('computes a complete url without consecutive slash', () => { 18 | const params: IApiUrlParams = { 19 | apiProto: 'https', 20 | baseUrl: 'api.starwars.galaxy', 21 | route: '///jedis////', 22 | version: '/v1///', 23 | }; 24 | 25 | expect(computeCompleteUrl(params)).toEqual('https://api.starwars.galaxy/v1/jedis/'); 26 | }); 27 | 28 | it('computes a complete url without parameters', () => { 29 | const params: IParametrizedApiUrlParams = { 30 | apiProto: 'https', 31 | baseUrl: 'api.starwars.galaxy', 32 | route: '/jedis', 33 | version: '/v1', 34 | }; 35 | 36 | expect(computeParametrizedUrl(params)).toEqual('https://api.starwars.galaxy/v1/jedis'); 37 | }); 38 | 39 | it('computes a complete url with parameters', () => { 40 | const params: IParametrizedApiUrlParams = { 41 | apiProto: 'https', 42 | baseUrl: 'api.starwars.galaxy', 43 | id: '5', 44 | queryParams: {}, 45 | route: '////jedis', 46 | version: '///v1', 47 | }; 48 | 49 | expect(computeParametrizedUrl(params)).toEqual('https://api.starwars.galaxy/v1/jedis/5/'); 50 | }); 51 | 52 | it('computes a complete url with parameters', () => { 53 | const params: IParametrizedApiUrlParams = { 54 | apiProto: 'https', 55 | baseUrl: 'api.starwars.galaxy', 56 | id: '5', 57 | queryParams: { hasForce: true }, 58 | route: '/jedis', 59 | version: '/v1', 60 | }; 61 | 62 | expect(computeParametrizedUrl(params)).toEqual('https://api.starwars.galaxy/v1/jedis/5/?hasForce=true'); 63 | }); 64 | 65 | it('computes empty header parameters', () => { 66 | expect(computeHeaders({})).toEqual({}); 67 | }); 68 | 69 | it('computes token and json header parameters', () => { 70 | expect(computeHeaders({ 71 | json: true, 72 | token: 'Bearer 1234', 73 | })).toEqual({ 74 | 'Authorization': 'Bearer 1234', 75 | 'Content-Type': 'application/json', 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/observableApiConnector/__tests__/streamFormatters-test.ts: -------------------------------------------------------------------------------- 1 | import { Observable, AjaxResponse, AjaxError } from 'rxjs'; 2 | 3 | import { formatResponse, formatError, formatAjaxStream } from '../streamFormatters'; 4 | 5 | describe('streamFormatters', () => { 6 | it('formats a ajax response', () => { 7 | const ajaxResponse = new AjaxResponse( 8 | new Event('ajaxResponse'), 9 | new XMLHttpRequest(), 10 | {}, 11 | ); 12 | 13 | ajaxResponse.response = 'OK'; 14 | 15 | expect(formatResponse(ajaxResponse)).toEqual('OK'); 16 | }); 17 | 18 | it('formats a ajax error', () => { 19 | const ajaxError = new AjaxError( 20 | 'Cannot find this jedi', 21 | new XMLHttpRequest(), 22 | {}, 23 | ); 24 | 25 | const apiError = formatError(ajaxError); 26 | 27 | expect(apiError).toBeInstanceOf(Error); 28 | expect(apiError.message).toEqual('Cannot find this jedi'); 29 | }); 30 | 31 | it('formats an ajax stream of one value', async () => { 32 | const ajaxResponse = new AjaxResponse( 33 | new Event('ajaxResponse'), 34 | new XMLHttpRequest(), 35 | {}, 36 | ); 37 | ajaxResponse.response = { 38 | id: '5', 39 | name: 'Yoda', 40 | }; 41 | 42 | const r = await formatAjaxStream(Observable.of(ajaxResponse)) 43 | .toPromise(); 44 | 45 | expect(r.id).toEqual('5'); 46 | expect(r.name).toEqual('Yoda'); 47 | }); 48 | 49 | it('formats an ajax stream of a list', async () => { 50 | const ajaxResponse = new AjaxResponse( 51 | new Event('ajaxResponse'), 52 | new XMLHttpRequest(), 53 | {}, 54 | ); 55 | ajaxResponse.response = { 56 | member: [ 57 | { 58 | id: '5', 59 | name: 'Yoda', 60 | }, 61 | { 62 | id: '6', 63 | name: 'Obi Wan', 64 | }, 65 | ], 66 | }; 67 | 68 | const r = await formatAjaxStream(Observable.of(ajaxResponse)) 69 | .toPromise(); 70 | 71 | expect(r.member[0].id).toEqual('5'); 72 | expect(r.member[0].name).toEqual('Yoda'); 73 | expect(r.member[1].id).toEqual('6'); 74 | expect(r.member[1].name).toEqual('Obi Wan'); 75 | }); 76 | 77 | it('formats an ajax stream throwing an error', async () => { 78 | const ajaxError = new AjaxError( 79 | 'Cannot find this jedi', 80 | new XMLHttpRequest(), 81 | {}, 82 | ); 83 | 84 | try { 85 | await formatAjaxStream(Observable.throw(ajaxError)) 86 | .toPromise(); 87 | } catch (e) { 88 | expect(e).toBeInstanceOf(Error); 89 | expect(e.message).toEqual('Cannot find this jedi'); 90 | } 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /src/actionsCreatorFactory/__tests__/deleteActionsCreatorFactory-test.ts: -------------------------------------------------------------------------------- 1 | import deleteActionsCreatorFactory from '../deleteActionsCreatorFactory'; 2 | 3 | describe('deleteActionsCreatorFactory', () => { 4 | const ENTITY = 'JEDI'; 5 | 6 | it('creates actions creators', () => { 7 | const actionsCreators = deleteActionsCreatorFactory(ENTITY); 8 | 9 | expect(actionsCreators).toBeDefined(); 10 | expect(actionsCreators.cancelDeleteEntity).toBeInstanceOf(Function); 11 | expect(actionsCreators.failDeleteEntity).toBeInstanceOf(Function); 12 | expect(actionsCreators.finishDeleteEntity).toBeInstanceOf(Function); 13 | expect(actionsCreators.requestDeleteEntity).toBeInstanceOf(Function); 14 | }); 15 | 16 | it('creates a cancel delete entity actions creator', () => { 17 | const { cancelDeleteEntity, cancelDeleteBatchEntities } = deleteActionsCreatorFactory(ENTITY); 18 | 19 | expect(cancelDeleteEntity()).toEqual({ 20 | type: 'CANCEL_DELETE_JEDI', 21 | }); 22 | 23 | expect(cancelDeleteBatchEntities()).toEqual({ 24 | type: 'CANCEL_DELETE_BATCH_JEDIS', 25 | }); 26 | }); 27 | 28 | it('creates a fail delete entity actions creator', () => { 29 | const { failDeleteEntity, failDeleteBatchEntities } = deleteActionsCreatorFactory(ENTITY); 30 | const payload = new Error(`Can't create this jedi`); 31 | 32 | expect(failDeleteEntity(payload)).toEqual({ 33 | error: true, 34 | payload, 35 | type: 'FAIL_DELETE_JEDI', 36 | }); 37 | 38 | expect(failDeleteBatchEntities(payload)).toEqual({ 39 | error: true, 40 | payload, 41 | type: 'FAIL_DELETE_BATCH_JEDIS', 42 | }); 43 | }); 44 | 45 | it('creates a finish delete entity actions creator', () => { 46 | const { finishDeleteEntity, finishDeleteBatchEntities } = deleteActionsCreatorFactory(ENTITY); 47 | const payload = { 48 | hash: '1234', 49 | name: 'Yoda', 50 | }; 51 | 52 | expect(finishDeleteEntity(payload)).toEqual({ 53 | payload, 54 | type: 'FINISH_DELETE_JEDI', 55 | }); 56 | 57 | expect(finishDeleteBatchEntities(payload)).toEqual({ 58 | payload, 59 | type: 'FINISH_DELETE_BATCH_JEDIS', 60 | }); 61 | }); 62 | 63 | it('creates a cancel delete entity actions creator', () => { 64 | const { requestDeleteEntity, requestDeleteBatchEntities } = deleteActionsCreatorFactory(ENTITY); 65 | const payload = { 66 | hash: '1234', 67 | }; 68 | 69 | expect(requestDeleteEntity(payload)).toEqual({ 70 | payload, 71 | type: 'REQUEST_DELETE_JEDI', 72 | }); 73 | 74 | expect(requestDeleteBatchEntities(payload)).toEqual({ 75 | payload, 76 | type: 'REQUEST_DELETE_BATCH_JEDIS', 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-crud-observable", 3 | "version": "1.1.1", 4 | "description": "Actions, reducers & epics for managing crud redux state", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "types": "lib/index.d.ts", 8 | "scripts": { 9 | "prepublish": "yarn compile", 10 | "pretest": "yarn lint", 11 | "preversion": "yarn test && yarn compile", 12 | "start": "yarn test:watch", 13 | "test": "yarn test:coverage", 14 | "test:once": "jest --colors", 15 | "test:watch": "jest --watch --watchExtensions ts,tsx,js --colors", 16 | "test:coverage": "jest --coverage --colors --no-cache --runInBand", 17 | "lint": "tslint -c tslint.json 'src/**/*.{ts,tsx}'", 18 | "compile": "yarn compile:es5 & yarn compile:es2015", 19 | "compile:es5": "tsc -p .", 20 | "compile:es2015": "tsc -p . --module es2015 --outDir ./es", 21 | "compile:watch": "yarn compile:es5 -- -w", 22 | "doc": "typedoc --mode modules --target ES6 --module ES6 --exclude **/*-test.ts --out doc src" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/FoodMeUp/redux-crud-observable.git" 27 | }, 28 | "author": "Thomas Hourlier (https://github.com/FoodMeUp)", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/FoodMeUp/redux-crud-observable/issues" 32 | }, 33 | "homepage": "https://github.com/FoodMeUp/redux-crud-observable#readme", 34 | "jest": { 35 | "testPathDirs": [ 36 | "src" 37 | ], 38 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$", 39 | "moduleDirectories": [ 40 | "node_modules", 41 | "src" 42 | ], 43 | "moduleFileExtensions": [ 44 | "ts", 45 | "tsx", 46 | "js", 47 | "json", 48 | "node" 49 | ], 50 | "transform": { 51 | ".(ts|tsx)": "/node_modules/ts-jest/preprocessor.js" 52 | }, 53 | "testResultsProcessor": "/node_modules/ts-jest/coverageprocessor.js", 54 | "testURL": "https://api.starwars.galaxy" 55 | }, 56 | "dependencies": { 57 | "debug": "^2.6.0", 58 | "immutable": "^3.8.1", 59 | "lodash": "^4.4.2", 60 | "qs": "^6.3.0", 61 | "redux-rac-utils": "^1.0.1" 62 | }, 63 | "devDependencies": { 64 | "@types/debug": "0.0.29", 65 | "@types/jest": "^18.1.1", 66 | "@types/lodash": "^4.4.1", 67 | "@types/nock": "^8.2.0", 68 | "@types/qs": "^6.2.30", 69 | "@types/redux-mock-store": "0.0.7", 70 | "gh-pages-travis": "^1.0.4", 71 | "jest": "^18.1.0", 72 | "nock": "^9.0.5", 73 | "redux": "^3.6.0", 74 | "redux-mock-store": "^1.2.2", 75 | "redux-observable": "^0.13.0", 76 | "reselect": "^2.5.4", 77 | "rxjs": "^5.1.0", 78 | "ts-jest": "^18.0.2", 79 | "tslint": "^4.4.2", 80 | "typedoc": "^0.5.7", 81 | "typescript": "^2.2.0", 82 | "yarn": "^0.21.3" 83 | }, 84 | "peerDependencies": { 85 | "redux": ">=3.6.0", 86 | "redux-observable": ">=0.13.0", 87 | "reselect": ">=2.5.4", 88 | "rxjs": ">=5.1.0" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/actionsCreatorFactory/__tests__/readActionsCreatorFactory-test.ts: -------------------------------------------------------------------------------- 1 | import readActionsCreatorFactory from '../readActionsCreatorFactory'; 2 | 3 | describe('readActionsCreatorFactory', () => { 4 | const ENTITY = 'JEDI'; 5 | 6 | it('creates actions creators', () => { 7 | const actionsCreators = readActionsCreatorFactory(ENTITY); 8 | 9 | expect(actionsCreators).toBeDefined(); 10 | expect(actionsCreators.cancelReadEntity).toBeInstanceOf(Function); 11 | expect(actionsCreators.failReadEntity).toBeInstanceOf(Function); 12 | expect(actionsCreators.finishReadEntity).toBeInstanceOf(Function); 13 | expect(actionsCreators.requestReadEntity).toBeInstanceOf(Function); 14 | }); 15 | 16 | it('creates a cancel read entity actions creator', () => { 17 | const { cancelReadEntity, cancelReadEntitiesList, cancelReadBatchEntities } = readActionsCreatorFactory(ENTITY); 18 | 19 | expect(cancelReadEntity()).toEqual({ 20 | type: 'CANCEL_READ_JEDI', 21 | }); 22 | 23 | expect(cancelReadEntitiesList()).toEqual({ 24 | type: 'CANCEL_READ_JEDIS_LIST', 25 | }); 26 | 27 | expect(cancelReadBatchEntities()).toEqual({ 28 | type: 'CANCEL_READ_BATCH_JEDIS', 29 | }); 30 | }); 31 | 32 | it('creates a fail read entity actions creator', () => { 33 | const { failReadEntity, failReadEntitiesList, failReadBatchEntities } = readActionsCreatorFactory(ENTITY); 34 | const payload = new Error(`Can't create this jedi`); 35 | 36 | expect(failReadEntity(payload)).toEqual({ 37 | error: true, 38 | payload, 39 | type: 'FAIL_READ_JEDI', 40 | }); 41 | 42 | expect(failReadEntitiesList(payload)).toEqual({ 43 | error: true, 44 | payload, 45 | type: 'FAIL_READ_JEDIS_LIST', 46 | }); 47 | 48 | expect(failReadBatchEntities(payload)).toEqual({ 49 | error: true, 50 | payload, 51 | type: 'FAIL_READ_BATCH_JEDIS', 52 | }); 53 | }); 54 | 55 | it('creates a finish read entity actions creator', () => { 56 | const { finishReadEntity, finishReadEntitiesList, finishReadBatchEntities } = readActionsCreatorFactory(ENTITY); 57 | const payload = { 58 | hash: '1234', 59 | name: 'Yoda', 60 | }; 61 | 62 | expect(finishReadEntity(payload)).toEqual({ 63 | payload, 64 | type: 'FINISH_READ_JEDI', 65 | }); 66 | 67 | expect(finishReadEntitiesList(payload)).toEqual({ 68 | payload, 69 | type: 'FINISH_READ_JEDIS_LIST', 70 | }); 71 | 72 | expect(finishReadBatchEntities(payload)).toEqual({ 73 | payload, 74 | type: 'FINISH_READ_BATCH_JEDIS', 75 | }); 76 | }); 77 | 78 | it('creates a cancel read entity actions creator', () => { 79 | const { requestReadEntity, requestReadEntitiesList, requestReadBatchEntities } = readActionsCreatorFactory(ENTITY); 80 | const payload = { 81 | hash: '1234', 82 | }; 83 | 84 | expect(requestReadEntity(payload)).toEqual({ 85 | payload, 86 | type: 'REQUEST_READ_JEDI', 87 | }); 88 | 89 | expect(requestReadEntitiesList(payload)).toEqual({ 90 | payload, 91 | type: 'REQUEST_READ_JEDIS_LIST', 92 | }); 93 | 94 | expect(requestReadBatchEntities(payload)).toEqual({ 95 | payload, 96 | type: 'REQUEST_READ_BATCH_JEDIS', 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "align": [ 4 | true, 5 | "parameters", 6 | "statements" 7 | ], 8 | "ban": false, 9 | "class-name": true, 10 | "comment-format": [ 11 | true, 12 | "check-space", 13 | "check-lowercase" 14 | ], 15 | "curly": false, 16 | "eofline": true, 17 | "forin": true, 18 | "indent": [ 19 | true, 20 | "spaces" 21 | ], 22 | "interface-name": true, 23 | "jsdoc-format": true, 24 | "label-position": true, 25 | "max-line-length": [ 26 | true, 27 | 140 28 | ], 29 | "member-access": true, 30 | "member-ordering": [ 31 | true, 32 | "public-before-private", 33 | "variables-before-functions" 34 | ], 35 | "no-any": false, 36 | "no-arg": true, 37 | "no-bitwise": true, 38 | "no-conditional-assignment": true, 39 | "no-consecutive-blank-lines": false, 40 | "no-console": [ 41 | true, 42 | "debug", 43 | "info", 44 | "time", 45 | "timeEnd", 46 | "trace" 47 | ], 48 | "no-construct": true, 49 | "no-debugger": true, 50 | "no-duplicate-variable": true, 51 | "no-empty": true, 52 | "no-eval": true, 53 | "no-inferrable-types": false, 54 | "no-internal-module": true, 55 | "no-null-keyword": true, 56 | "no-require-imports": false, 57 | "no-shadowed-variable": true, 58 | "no-string-literal": true, 59 | "no-switch-case-fall-through": true, 60 | "no-trailing-whitespace": true, 61 | "no-unused-expression": true, 62 | "no-use-before-declare": true, 63 | "no-var-keyword": true, 64 | "no-var-requires": true, 65 | "object-literal-sort-keys": true, 66 | "one-line": [ 67 | true, 68 | "check-open-brace", 69 | "check-catch", 70 | "check-else", 71 | "check-finally", 72 | "check-whitespace" 73 | ], 74 | "quotemark": [ 75 | true, 76 | "single", 77 | "jsx-double", 78 | "avoid-escape" 79 | ], 80 | "radix": true, 81 | "semicolon": [ 82 | true, 83 | "always" 84 | ], 85 | "switch-default": true, 86 | "trailing-comma": [ 87 | true, 88 | { 89 | "multiline": "always", 90 | "singleline": "never" 91 | } 92 | ], 93 | "triple-equals": [ 94 | true, 95 | "allow-null-check" 96 | ], 97 | "typedef": [ 98 | true, 99 | "call-signature", 100 | "parameter", 101 | "property-declaration", 102 | "member-variable-declaration" 103 | ], 104 | "typedef-whitespace": [ 105 | true, 106 | { 107 | "call-signature": "nospace", 108 | "index-signature": "nospace", 109 | "parameter": "nospace", 110 | "property-declaration": "nospace", 111 | "variable-declaration": "nospace" 112 | }, 113 | { 114 | "call-signature": "space", 115 | "index-signature": "space", 116 | "parameter": "space", 117 | "property-declaration": "space", 118 | "variable-declaration": "space" 119 | } 120 | ], 121 | "variable-name": [ 122 | true, 123 | "allow-leading-underscore", 124 | "ban-keywords" 125 | ], 126 | "whitespace": [ 127 | true, 128 | "check-branch", 129 | "check-decl", 130 | "check-operator", 131 | "check-separator", 132 | "check-type" 133 | ] 134 | } 135 | } -------------------------------------------------------------------------------- /src/observableApiConnector/__tests__/observableApiConnector-test.ts: -------------------------------------------------------------------------------- 1 | jest.mock('rxjs/observable/dom/ajax'); 2 | jest.mock('../requestFormatters'); 3 | jest.mock('../streamFormatters'); 4 | 5 | import { ajax } from 'rxjs/observable/dom/ajax'; 6 | import { formatAjaxStream } from '../streamFormatters'; 7 | import { computeHeaders, computeParametrizedUrl } from '../requestFormatters'; 8 | 9 | import { 10 | createAjaxStream, 11 | readEntity, 12 | deleteEntity, 13 | updateEntity, 14 | createEntity, 15 | } from '../observableApiConnector'; 16 | 17 | describe('observableApiConnector', () => { 18 | it('creates an ajax stream an entity', () => { 19 | createAjaxStream({ 20 | body: { master: true }, 21 | config: { 22 | apiProto: 'https', 23 | baseUrl: 'api.starwars.galaxy', 24 | json: false, 25 | route: '/jedis', 26 | token: 'Bearer 1234', 27 | version: '/v1', 28 | }, 29 | id: 5, 30 | method: 'RANDOM', 31 | queryParams: { hasForce: true }, 32 | responseType: 'json', 33 | }); 34 | 35 | expect((>computeHeaders).mock.calls[0][0]).toEqual({ 36 | json: false, 37 | token: 'Bearer 1234', 38 | }); 39 | 40 | expect((>computeParametrizedUrl).mock.calls[0][0]).toEqual({ 41 | apiProto: 'https', 42 | baseUrl: 'api.starwars.galaxy', 43 | id: 5, 44 | queryParams: { hasForce: true }, 45 | route: '/jedis', 46 | version: '/v1', 47 | }); 48 | 49 | expect((>ajax).mock.calls[0][0]).toEqual({ 50 | body: { master: true }, 51 | crossDomain: true, 52 | method: 'RANDOM', 53 | responseType: 'json', 54 | }); 55 | 56 | expect((>formatAjaxStream).mock.calls[0][0]).toEqual(undefined); 57 | }); 58 | 59 | it('reads an entity', () => { 60 | readEntity({ 61 | config: { 62 | apiProto: 'https', 63 | baseUrl: 'api.starwars.galaxy', 64 | route: '/jedis', 65 | version: '/v1', 66 | }, 67 | }); 68 | 69 | expect((>ajax).mock.calls[1][0]).toEqual({ 70 | crossDomain: true, 71 | method: 'GET', 72 | responseType: 'json', 73 | }); 74 | }); 75 | 76 | it('deletes a list of entities', () => { 77 | deleteEntity({ 78 | config: { 79 | apiProto: 'https', 80 | baseUrl: 'api.starwars.galaxy', 81 | route: '/jedis', 82 | version: '/v1', 83 | }, 84 | }); 85 | 86 | expect((>ajax).mock.calls[2][0]).toEqual({ 87 | crossDomain: true, 88 | method: 'DELETE', 89 | }); 90 | }); 91 | 92 | it('updates an entity', () => { 93 | updateEntity({ 94 | body: { master: true }, 95 | config: { 96 | apiProto: 'https', 97 | baseUrl: 'api.starwars.galaxy', 98 | route: '/jedis', 99 | version: '/v1', 100 | }, 101 | }); 102 | 103 | expect((>ajax).mock.calls[3][0]).toEqual({ 104 | body: { master: true }, 105 | crossDomain: true, 106 | method: 'PUT', 107 | responseType: 'json', 108 | }); 109 | }); 110 | 111 | it('creates an entity', () => { 112 | createEntity({ 113 | body: { master: true }, 114 | config: { 115 | apiProto: 'https', 116 | baseUrl: 'api.starwars.galaxy', 117 | route: '/jedis', 118 | version: '/v1', 119 | }, 120 | 121 | }); 122 | 123 | expect((>ajax).mock.calls[4][0]).toEqual({ 124 | body: { master: true }, 125 | crossDomain: true, 126 | method: 'POST', 127 | responseType: 'json', 128 | }); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /src/epicFactory/readEpicFactory.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { ActionsObservable, Epic, combineEpics } from 'redux-observable'; 3 | 4 | import { 5 | readEntity, 6 | } from 'observableApiConnector'; 7 | import { 8 | IEntity, 9 | IEntitiesList, 10 | resolveEntityKey, 11 | resolveEntitiesKey, 12 | resolveEntitiesListKey, 13 | } from 'crudEntity'; 14 | import { 15 | readCrudActionsCreatorFactory, 16 | IRequestReadEntityAction, 17 | IRequestReadEntitiesAction, 18 | IRequestReadEntitiesListAction, 19 | } from 'actionsCreatorFactory'; 20 | import { 21 | READ, 22 | READ_LIST, 23 | READ_BATCH, 24 | } from 'constantFactory'; 25 | 26 | import computeApiConfig from './computeApiConfig'; 27 | import { IEpicParams } from './interfaces'; 28 | 29 | export default function readEpicFactory({ 30 | entity, 31 | apiConfig, 32 | }: IEpicParams): Epic { 33 | const { 34 | finishReadEntity, 35 | failReadEntity, 36 | 37 | finishReadEntitiesList, 38 | failReadEntitiesList, 39 | 40 | finishReadBatchEntities, 41 | failReadBatchEntities, 42 | } = readCrudActionsCreatorFactory(entity); 43 | 44 | const readEntityEpic: Epic = (action$: ActionsObservable) => ( 45 | action$.ofType(READ(entity).REQUEST) 46 | .switchMap(({ meta, payload }: IRequestReadEntityAction) => { 47 | if (!payload) return Observable.empty(); 48 | 49 | const config = computeApiConfig(apiConfig, payload.api); 50 | 51 | return readEntity({ 52 | config: config, 53 | id: payload.id, 54 | queryParams: payload.queryParams, 55 | }) 56 | .map((res: any) => resolveEntityKey(res)) 57 | .map((res: IEntity) => finishReadEntity(res, meta)) 58 | .takeUntil(action$.ofType(READ(entity).CANCEL)) 59 | .catch((error: Error) => Observable.of(failReadEntity(error, meta))); 60 | }) 61 | ); 62 | 63 | const readEntitiesListEpic: Epic = (action$: ActionsObservable) => ( 64 | action$.ofType(READ_LIST(entity).REQUEST) 65 | .switchMap(({ meta, payload }: IRequestReadEntitiesListAction) => { 66 | const config = computeApiConfig(apiConfig, payload ? payload.api : undefined); 67 | 68 | return readEntity({ 69 | config: config, 70 | queryParams: payload && payload.queryParams, 71 | }) 72 | .map((res: any) => resolveEntitiesListKey(res)) 73 | .map((res: IEntitiesList) => finishReadEntitiesList(res, meta)) 74 | .takeUntil(action$.ofType(READ_LIST(entity).CANCEL)) 75 | .catch((error: Error) => Observable.of(failReadEntitiesList(error, meta))); 76 | }) 77 | ); 78 | 79 | const readBatchEntitiesEpic: Epic = (action$: ActionsObservable) => ( 80 | action$.ofType(READ_BATCH(entity).REQUEST) 81 | .switchMap(({ meta, payload }: IRequestReadEntitiesAction) => { 82 | if (!payload) return Observable.empty(); 83 | 84 | const config = computeApiConfig(apiConfig, payload.api); 85 | 86 | return Observable 87 | .forkJoin( 88 | payload.ids.map(id => readEntity({ 89 | config: config, 90 | id, 91 | queryParams: payload.queryParams, 92 | })), 93 | ) 94 | .map((res: any) => resolveEntitiesKey(res)) 95 | .map((res: Array) => finishReadBatchEntities(res, meta)) 96 | .takeUntil(action$.ofType(READ_BATCH(entity).CANCEL)) 97 | .catch((error: Error) => Observable.of(failReadBatchEntities(error, meta))); 98 | }) 99 | ); 100 | 101 | return combineEpics( 102 | readEntityEpic, 103 | readEntitiesListEpic, 104 | readBatchEntitiesEpic, 105 | ); 106 | } 107 | -------------------------------------------------------------------------------- /src/__tests__/reduxCrudObservable-test.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { createStore, applyMiddleware, Store } from 'redux'; 3 | import { createEpicMiddleware } from 'redux-observable'; 4 | import * as nock from 'nock'; 5 | 6 | import { CrudState } from 'reducerFactory'; 7 | 8 | import * as reduxCrudObservable from '../index'; 9 | 10 | function toObservable(store: Store): Observable { 11 | return new Observable((obs: any) => { 12 | store.subscribe(() => obs.next(store.getState())); 13 | }); 14 | } 15 | 16 | function firstTwoStates(store: Store, expectations: (states: any[]) => void): Promise { 17 | return new Promise((resolve, reject) => { 18 | toObservable(store) 19 | .bufferCount(2) 20 | .subscribe((states: any[]) => { 21 | try { 22 | expectations(states); 23 | resolve(); 24 | } catch (e) { 25 | reject(e); 26 | } 27 | }); 28 | }); 29 | } 30 | 31 | describe('redux-crud-observable', () => { 32 | const ENTITY = 'JEDI'; 33 | const rootEpic = reduxCrudObservable.crudEpicFactory({ 34 | apiConfig: { 35 | apiProto: 'https', 36 | baseUrl: 'api.starwars.galaxy', 37 | json: false, 38 | route: '/jedis', 39 | token: 'Bearer 1234', 40 | version: '/v1', 41 | }, 42 | entity: ENTITY, 43 | }); 44 | let store: Store; 45 | let mockServer: nock.Scope; 46 | 47 | const { 48 | requestReadEntity, 49 | cancelReadEntity, 50 | } = reduxCrudObservable.crudActionsCreatorFactory(ENTITY); 51 | 52 | beforeEach(() => { 53 | reduxCrudObservable.setEntityKey('apiHash'); 54 | 55 | const rootReducer = reduxCrudObservable.crudReducerFactory(ENTITY); 56 | store = createStore(rootReducer, applyMiddleware(createEpicMiddleware(rootEpic))); 57 | mockServer = nock('https://api.starwars.galaxy', { 58 | reqheaders: { 59 | 'Authorization': 'Bearer 1234', 60 | }, 61 | }); 62 | }); 63 | 64 | afterEach(() => { 65 | reduxCrudObservable.resetConfig(); 66 | nock.cleanAll(); 67 | }); 68 | 69 | it('reads an entity and store it into the crud reducer', () => { 70 | mockServer 71 | .get('/v1/jedis/5') 72 | .reply(200, { 73 | apiHash: 5, 74 | name: 'Yoda', 75 | }); 76 | 77 | const p = firstTwoStates(store, (states) => { 78 | expect(states[0].get('value').toJS()).toEqual({}); 79 | expect(states[1].get('value').toJS()).toEqual({ 80 | 5: { _internalHash: 5, apiHash: 5, name: 'Yoda' }, 81 | }); 82 | }); 83 | 84 | store.dispatch(requestReadEntity({ 85 | id: 5, 86 | })); 87 | 88 | return p; 89 | }); 90 | 91 | it('fails to read an entity and store it into the crud reducer', () => { 92 | mockServer 93 | .get('/v1/jedis/5') 94 | .reply(404); 95 | 96 | const p = firstTwoStates(store, (states) => { 97 | expect(states[0].get('value').toJS()).toEqual({}); 98 | expect(states[1].get('value').toJS()).toEqual({}); 99 | }); 100 | 101 | store.dispatch(requestReadEntity({ 102 | id: 5, 103 | })); 104 | 105 | return p; 106 | }); 107 | 108 | it('cancels reading an entity and store it into the crud reducer', () => { 109 | mockServer 110 | .get('/v1/jedis/5') 111 | .delay(100) 112 | .reply(200, { 113 | hash: 5, 114 | name: 'Yoda', 115 | }); 116 | 117 | const p = firstTwoStates(store, (states) => { 118 | expect(states[0].get('value').toJS()).toEqual({}); 119 | expect(states[1].get('value').toJS()).toEqual({}); 120 | }); 121 | 122 | store.dispatch(requestReadEntity({ 123 | id: 5, 124 | })); 125 | store.dispatch(cancelReadEntity()); 126 | 127 | return p; 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /src/observableApiConnector/__tests__/observableApiConnector-func-test.ts: -------------------------------------------------------------------------------- 1 | import * as nock from 'nock'; 2 | 3 | import { 4 | readEntity, 5 | deleteEntity, 6 | createEntity, 7 | updateEntity, 8 | } from '../observableApiConnector'; 9 | 10 | describe('functionnal observableApiConnector', () => { 11 | let mockServer: nock.Scope; 12 | 13 | beforeEach(() => { 14 | mockServer = nock('https://api.starwars.galaxy', { 15 | reqheaders: { 16 | 'Authorization': 'Bearer 1234', 17 | }, 18 | }); 19 | }); 20 | 21 | afterEach(() => { 22 | nock.cleanAll(); 23 | }); 24 | 25 | it('reads an entity', async () => { 26 | mockServer 27 | .get('/v1/jedis/5/?hasForce=true') 28 | .reply(200, { 29 | id: 5, 30 | name: 'Yoda', 31 | }); 32 | 33 | const stream$ = readEntity({ 34 | config: { 35 | apiProto: 'https', 36 | baseUrl: 'api.starwars.galaxy', 37 | json: false, 38 | route: '/jedis', 39 | token: 'Bearer 1234', 40 | version: '/v1', 41 | }, 42 | id: 5, 43 | queryParams: { hasForce: true }, 44 | }); 45 | 46 | try { 47 | const res = await stream$.toPromise(); 48 | 49 | expect(res).toEqual({ 50 | id: 5, 51 | name: 'Yoda', 52 | }); 53 | } catch (e) { 54 | throw e; 55 | } 56 | }); 57 | 58 | it('reads a list of entities', async () => { 59 | mockServer 60 | .get('/v1/jedis') 61 | .reply(200, { 62 | member: [ 63 | { 64 | id: 5, 65 | name: 'Yoda', 66 | }, 67 | { 68 | id: 42, 69 | name: 'Obi Wan', 70 | }, 71 | ], 72 | }); 73 | 74 | const stream$ = readEntity({ 75 | config: { 76 | apiProto: 'https', 77 | baseUrl: 'api.starwars.galaxy', 78 | json: false, 79 | route: '/jedis', 80 | token: 'Bearer 1234', 81 | version: '/v1', 82 | }, 83 | }); 84 | 85 | try { 86 | const res = await stream$.toPromise(); 87 | 88 | expect(res).toEqual({ 89 | member: [ 90 | { 91 | id: 5, 92 | name: 'Yoda', 93 | }, 94 | { 95 | id: 42, 96 | name: 'Obi Wan', 97 | }, 98 | ], 99 | }); 100 | } catch (e) { 101 | throw e; 102 | } 103 | }); 104 | 105 | it('deletes an entity', async () => { 106 | mockServer 107 | .delete('/v1/jedis/5') 108 | .reply(204); 109 | 110 | const stream$ = deleteEntity({ 111 | config: { 112 | apiProto: 'https', 113 | baseUrl: 'api.starwars.galaxy', 114 | json: false, 115 | route: '/jedis', 116 | token: 'Bearer 1234', 117 | version: '/v1', 118 | }, 119 | id: 5, 120 | }); 121 | 122 | try { 123 | const res = await stream$.toPromise(); 124 | 125 | expect(res).toEqual(''); 126 | } catch (e) { 127 | throw e; 128 | } 129 | }); 130 | 131 | it('updates an entity', async () => { 132 | mockServer 133 | .put('/v1/jedis/5') 134 | .reply(200, { 135 | id: 5, 136 | name: 'Obi Wan', 137 | }); 138 | 139 | const stream$ = updateEntity({ 140 | body: { 141 | name: 'Obi Wan', 142 | }, 143 | config: { 144 | apiProto: 'https', 145 | baseUrl: 'api.starwars.galaxy', 146 | json: false, 147 | route: '/jedis', 148 | token: 'Bearer 1234', 149 | version: '/v1', 150 | }, 151 | id: 5, 152 | }); 153 | 154 | try { 155 | const res = await stream$.toPromise(); 156 | 157 | expect(res).toEqual({ 158 | id: 5, 159 | name: 'Obi Wan', 160 | }); 161 | } catch (e) { 162 | throw e; 163 | } 164 | }); 165 | 166 | it('creates an entity', async () => { 167 | mockServer 168 | .post('/v1/jedis') 169 | .reply(201, { 170 | id: 5, 171 | name: 'Obi Wan', 172 | }); 173 | 174 | const stream$ = createEntity({ 175 | body: { 176 | name: 'Obi Wan', 177 | }, 178 | config: { 179 | apiProto: 'https', 180 | baseUrl: 'api.starwars.galaxy', 181 | json: false, 182 | route: '/jedis', 183 | token: 'Bearer 1234', 184 | version: '/v1', 185 | }, 186 | }); 187 | 188 | try { 189 | const res = await stream$.toPromise(); 190 | 191 | expect(res).toEqual({ 192 | id: 5, 193 | name: 'Obi Wan', 194 | }); 195 | } catch (e) { 196 | throw e; 197 | } 198 | }); 199 | }); 200 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redux-crud-observable 2 | [![Build Status](https://travis-ci.org/FoodMeUp/redux-crud-observable.svg?branch=master)](https://travis-ci.org/FoodMeUp/redux-crud-observable) 3 | [![codecov](https://codecov.io/gh/FoodMeUp/redux-crud-observable/branch/master/graph/badge.svg)](https://codecov.io/gh/FoodMeUp/redux-crud-observable) 4 | 5 | Ever been tired of manipulating CRUD entities in a redux app? You always have to do the same things again and again: 6 | fetching an entity, doing the corresponding ajax call, waiting for the promise resolution, storing the value in a reducer... Boring. 7 | 8 | **redux-crud-observable** is doing all of that for you. You only have to setup your entity name and your api configuration. It will handle the rest for you, ie: 9 | * generating the corresponding actions creators 10 | * generating the corresponding reducer 11 | * generating the corresponding selectors 12 | * handling all ajax calls 13 | 14 | It also comes with a very handy feature: request cancellation. It means that you can cancel at any time a request that is already flying ✈️. 15 | 16 | ## Getting Started 17 | 18 | Simply run: 19 | ```js 20 | npm install --save redux-crud-observable 21 | ``` 22 | 23 | It also has [peer dependencies](https://github.com/FoodMeUp/redux-crud-observable/blob/e0540947757cc7375b741a71a93ee76a3eeed9bd/package.json#L83): **redux, redux-observable, rxjs and reselect**. 24 | 25 | ## Doc 26 | You could browse the project documentation [here](https://foodmeup.github.io/redux-crud-observable/index.html). 27 | 28 | ## Usage 29 | 30 | You could check an exemple in [this](https://github.com/FoodMeUp/redux-crud-observable/blob/7fa12ab1d3f73b2c0170d1cb58402a5176ba14a1/src/__tests__/reduxCrudObservable-test.ts#L66) functionnal test. 31 | 32 | ### Configuration 33 | 34 | To use this library, you will have to add the `createEpicMiddleware` from **redux-observable** to your store enhancers. 35 | Here is a bery basic setup. 36 | 37 | ```js 38 | import { createStore, applyMiddleware } from 'redux'; 39 | import { createEpicMiddleware } from 'redux-observable'; 40 | import { epicFactory, crudReducerFactory } from 'redux-crud-observable'; 41 | 42 | const ENTITY = 'JEDI'; 43 | const crudReducer = crudReducerFactory(ENTITY); 44 | const rootReducer = combineReducers({ 45 | crud: crudReducer, 46 | }); 47 | const rootEpic = reduxCrudObservable.crudEpicFactory({ 48 | apiConfig: { 49 | apiProto: 'https', 50 | baseUrl: 'api.starwars.galaxy', 51 | json: false, 52 | route: '/jedis', 53 | token: 'Bearer 1234', 54 | version: '/v1', 55 | }, 56 | entity: ENTITY, 57 | }); 58 | 59 | const store = createStore(rootReducer, applyMiddleware(createEpicMiddleware(rootEpic))); 60 | ``` 61 | 62 | ### Actions Creators 63 | 64 | You can generate and use crud actions creators like that: 65 | 66 | ```js 67 | 68 | import { crudActionsCreatorFactory } from 'redux-crud-observable'; 69 | 70 | const { 71 | requestReadEntity, 72 | cancelReadEntity, 73 | } = reduxCrudObservable.crudActionsCreatorFactory('JEDI'); 74 | 75 | console.log(requestReadEntity({ 76 | id: 5 77 | })); // { type: 'REQUEST_READ_JEDI', payload: 5 } 78 | 79 | ``` 80 | 81 | The full list of actions creators is available [here](https://foodmeup.github.io/redux-crud-observable/interfaces/_actionscreatorfactory_interfaces_crudactions_.icrudactionscreators.html). 82 | 83 | ### Reducer 84 | 85 | You can create the crud reducer and use it like that: 86 | 87 | ```js 88 | 89 | import { combineReducers } from 'redux'; 90 | import { crudReducerFactory } from 'redux-crud-observable'; 91 | 92 | const crudReducer = crudReducerFactory('JEDI'); 93 | 94 | export default combineReducers({ 95 | crud: crudReducer, 96 | }); 97 | 98 | ``` 99 | 100 | ### Epic 101 | 102 | You can create the root crud epic like that: 103 | 104 | ```js 105 | import { crudEpicFactory } from 'redux-crud-observable'; 106 | 107 | const rootEpic = crudEpicFactory({ 108 | apiConfig: { 109 | apiProto: 'https', 110 | baseUrl: 'api.starwars.galaxy', 111 | json: false, 112 | route: '/jedis', 113 | token: 'Bearer 1234', 114 | version: '/v1', 115 | }, 116 | entity: 'JEDI', 117 | }); 118 | ``` 119 | 120 | ### Selectors 121 | 122 | You can create some reselect selectors like that: 123 | 124 | ```js 125 | 126 | import { crudSelectorFactory } from 'redux-crud-observable'; 127 | 128 | const { entitiesValueSelector } = crudSelectorFactory(['crud']); 129 | 130 | console.log(entitiesValueSelector(state)); => // Immutable.Map({ 1234: { hash: 1234, name: 'Yoda' } }); 131 | ``` 132 | 133 | [Here](https://foodmeup.github.io/redux-crud-observable/interfaces/_selectorfactory_interfaces_selectorfactory_.icrudselectors.html) is the list of available selectors. 134 | 135 | ## Roadmap 136 | 137 | * Make the API configuration more generic ([current implementation](https://foodmeup.github.io/redux-crud-observable/interfaces/_observableapiconnector_interfaces_requestformatters_.iapiconfig.html)) 138 | -------------------------------------------------------------------------------- /src/reducerFactory/__tests__/reducerFactory-test.ts: -------------------------------------------------------------------------------- 1 | import { Reducer } from 'redux'; 2 | import { 3 | INIT_STORE, 4 | CREATE, 5 | READ, 6 | READ_BATCH, 7 | READ_LIST, 8 | UPDATE, 9 | DELETE, 10 | DELETE_BATCH, 11 | } from 'constantFactory'; 12 | 13 | import { CrudState } from '../interfaces'; 14 | import crudReducerFactory from '../reducerFactory'; 15 | 16 | describe('Crud Reducer Factory', () => { 17 | const ENTITY = 'JEDI'; 18 | 19 | it('throws an error if the factory has invalid arguments', () => { 20 | expect(() => crudReducerFactory(undefined)) 21 | .toThrowError('ENTITY is missing'); 22 | }); 23 | 24 | it('creates a crud reducer', () => { 25 | const reducer = crudReducerFactory(ENTITY); 26 | 27 | expect(reducer).toBeDefined(); 28 | }); 29 | 30 | it('upgrades the initial reducer state', () => { 31 | const initialState = { 32 | myPadawan: 'Anakin', 33 | }; 34 | 35 | const reducer = crudReducerFactory(ENTITY, initialState); 36 | const state = reducer(undefined, undefined); 37 | 38 | expect(state.get('value').toJS()).toEqual({}); 39 | expect(state.get('myPadawan')).toEqual('Anakin'); 40 | }); 41 | 42 | it('upgrades the reducer reducers', () => { 43 | const actionType = 'ACTION_TYPE'; 44 | const actionPayload = 5; 45 | const reducersMap = { 46 | [actionType]: (state: any, action: any) => state.setIn(['value'], action.payload), 47 | }; 48 | 49 | const reducer = crudReducerFactory(ENTITY, undefined, reducersMap); 50 | 51 | const state = reducer(undefined, { 52 | payload: actionPayload, 53 | type: actionType, 54 | }); 55 | 56 | expect(state.get('value')).toEqual(actionPayload); 57 | }); 58 | 59 | describe('with a CRUD reducer', () => { 60 | let reducer: Reducer; 61 | const yoda = { 62 | _internalHash: '1234', 63 | name: 'Yoda', 64 | }; 65 | const obiWan = { 66 | _internalHash: '5678', 67 | name: 'Obi Wan', 68 | }; 69 | const anakin = { 70 | _internalHash: '9012', 71 | name: 'Anakin', 72 | }; 73 | const jedis = [yoda, obiWan]; 74 | 75 | beforeEach(() => { 76 | reducer = crudReducerFactory(ENTITY, { 77 | value: { 78 | 9012: anakin, 79 | }, 80 | }); 81 | }); 82 | 83 | describe('STORE', () => { 84 | it('inits the reducer store', () => { 85 | const now = new Date(); 86 | const action = { 87 | payload: { now }, 88 | type: INIT_STORE(ENTITY), 89 | }; 90 | const state = reducer(undefined, action); 91 | 92 | expect(state.get('bootTime')).toEqual(now); 93 | }); 94 | }); 95 | 96 | describe('CREATE', () => { 97 | it('creates an entity', () => { 98 | const action = { 99 | payload: yoda, 100 | type: CREATE(ENTITY).FINISH, 101 | }; 102 | const state = reducer(undefined, action); 103 | 104 | expect(state.getIn(['value', '1234']).toJS()).toEqual(yoda); 105 | }); 106 | 107 | it('creates an entity without a payload', () => { 108 | const initialState = reducer(undefined, undefined); 109 | 110 | const action = { 111 | payload: undefined, 112 | type: CREATE(ENTITY).FINISH, 113 | }; 114 | const state = reducer(undefined, action); 115 | 116 | expect(state).toEqual(initialState); 117 | }); 118 | }); 119 | 120 | describe('READ', () => { 121 | it('reads an entity without a payload', () => { 122 | const initialState = reducer(undefined, undefined); 123 | 124 | const action = { 125 | payload: undefined, 126 | type: READ(ENTITY).FINISH, 127 | }; 128 | const state = reducer(undefined, action); 129 | 130 | expect(state).toEqual(initialState); 131 | }); 132 | 133 | it('reads an entity', () => { 134 | const action = { 135 | payload: yoda, 136 | type: READ(ENTITY).FINISH, 137 | }; 138 | const state = reducer(undefined, action); 139 | 140 | expect(state.getIn(['value', '1234']).toJS()).toEqual(yoda); 141 | }); 142 | 143 | it('reads a batch of entities without a payload', () => { 144 | const initialState = reducer(undefined, undefined); 145 | 146 | const action = { 147 | payload: undefined, 148 | type: READ_BATCH(ENTITY).FINISH, 149 | }; 150 | const state = reducer(undefined, action); 151 | 152 | expect(state).toEqual(initialState); 153 | }); 154 | 155 | it('reads a batch of entities', () => { 156 | const action = { 157 | payload: jedis, 158 | type: READ_BATCH(ENTITY).FINISH, 159 | }; 160 | const state = reducer(undefined, action); 161 | 162 | expect(state.getIn(['value', '1234']).toJS()).toEqual(yoda); 163 | expect(state.getIn(['value', '5678']).toJS()).toEqual(obiWan); 164 | }); 165 | 166 | it('reads a list of entities without a payload', () => { 167 | const initialState = reducer(undefined, undefined); 168 | 169 | const action = { 170 | payload: undefined, 171 | type: READ_LIST(ENTITY).FINISH, 172 | }; 173 | const state = reducer(undefined, action); 174 | 175 | expect(state).toEqual(initialState); 176 | }); 177 | 178 | it('reads a list of entities', () => { 179 | const action = { 180 | payload: { 181 | _internalMember: jedis, 182 | }, 183 | type: READ_LIST(ENTITY).FINISH, 184 | }; 185 | const state = reducer(undefined, action); 186 | 187 | expect(state.getIn(['value', '1234']).toJS()).toEqual(yoda); 188 | expect(state.getIn(['value', '5678']).toJS()).toEqual(obiWan); 189 | }); 190 | }); 191 | 192 | describe('UPDATE', () => { 193 | it('updates an entity without a payload', () => { 194 | const initialState = reducer(undefined, undefined); 195 | 196 | const action = { 197 | payload: undefined, 198 | type: UPDATE(ENTITY).FINISH, 199 | }; 200 | const state = reducer(undefined, action); 201 | 202 | expect(state).toEqual(initialState); 203 | }); 204 | 205 | it('updates an entity', () => { 206 | const darkVador = { 207 | _internalHash: '9012', 208 | name: 'Dark Vador', 209 | }; 210 | const action = { 211 | payload: darkVador, 212 | type: UPDATE(ENTITY).FINISH, 213 | }; 214 | const state = reducer(undefined, action); 215 | 216 | expect(state.getIn(['value', '9012']).toJS()).toEqual(darkVador); 217 | }); 218 | }); 219 | 220 | describe('DELETE', () => { 221 | it('deletes an entity without a payload', () => { 222 | const initialState = reducer(undefined, undefined); 223 | 224 | const action = { 225 | payload: undefined, 226 | type: DELETE(ENTITY).FINISH, 227 | }; 228 | const state = reducer(undefined, action); 229 | 230 | expect(state).toEqual(initialState); 231 | }); 232 | 233 | it('deletes an entity', () => { 234 | const action = { 235 | payload: anakin, 236 | type: DELETE(ENTITY).FINISH, 237 | }; 238 | const state = reducer(undefined, action); 239 | 240 | expect(state.hasIn(['value', '9012'])).toBeFalsy(); 241 | }); 242 | 243 | it('deletes a batch of entities', () => { 244 | const action = { 245 | payload: [anakin], 246 | type: DELETE_BATCH(ENTITY).FINISH, 247 | }; 248 | const state = reducer(undefined, action); 249 | 250 | expect(state.hasIn(['value', '9012'])).toBeFalsy(); 251 | }); 252 | 253 | it('deletes a batch of entities without a payload', () => { 254 | const initialState = reducer(undefined, undefined); 255 | 256 | const action = { 257 | payload: undefined, 258 | type: DELETE_BATCH(ENTITY).FINISH, 259 | }; 260 | const state = reducer(undefined, action); 261 | 262 | expect(state).toEqual(initialState); 263 | }); 264 | }); 265 | }); 266 | }); 267 | -------------------------------------------------------------------------------- /src/epicFactory/__tests__/epicFactory-test.ts: -------------------------------------------------------------------------------- 1 | import * as nock from 'nock'; 2 | import { ActionsObservable, Epic } from 'redux-observable'; 3 | import configureMockStore, { IStore } from 'redux-mock-store'; 4 | 5 | import { IApiConfig } from 'observableApiConnector'; 6 | import { crudActionsCreatorFactory } from 'actionsCreatorFactory'; 7 | 8 | import epicFactory from '../epicFactory'; 9 | 10 | describe('epicFactory', () => { 11 | let mockServer: nock.Scope; 12 | let store: IStore; 13 | let rootEpic: Epic; 14 | 15 | const entity = 'JEDI'; 16 | const apiConfig: IApiConfig = { 17 | apiProto: 'https', 18 | baseUrl: 'api.starwars.galaxy', 19 | route: '/jedis', 20 | version: '/v1', 21 | }; 22 | 23 | const { 24 | cancelReadEntity, 25 | cancelReadEntitiesList, 26 | cancelReadBatchEntities, 27 | cancelCreateEntity, 28 | cancelUpdateEntity, 29 | cancelDeleteEntity, 30 | cancelDeleteBatchEntities, 31 | 32 | requestReadEntity, 33 | requestReadEntitiesList, 34 | requestReadBatchEntities, 35 | requestCreateEntity, 36 | requestUpdateEntity, 37 | requestDeleteEntity, 38 | requestDeleteBatchEntities, 39 | } = crudActionsCreatorFactory(entity); 40 | const mockStore = configureMockStore(); 41 | 42 | beforeEach(() => { 43 | mockServer = nock('https://api.starwars.galaxy'); 44 | store = mockStore(); 45 | rootEpic = epicFactory({ 46 | entity, 47 | apiConfig, 48 | }); 49 | }); 50 | 51 | afterEach(() => { 52 | nock.cleanAll(); 53 | }); 54 | 55 | describe('Create Epic', () => { 56 | it('creates a create entity epic with bad params', () => { 57 | const input$ = ActionsObservable.of(requestCreateEntity()); 58 | 59 | return new Promise((resolve, reject) => { 60 | rootEpic(input$, store) 61 | .isEmpty() 62 | .subscribe((actions: boolean) => { 63 | try { 64 | expect(actions).toBeTruthy(); 65 | 66 | resolve(); 67 | } catch (e) { 68 | reject(e); 69 | } 70 | }); 71 | }); 72 | }); 73 | 74 | it('creates a create entity epic', () => { 75 | const fixture = { 76 | id: 5, 77 | name: 'Yoda', 78 | }; 79 | 80 | mockServer 81 | .post('/v1/jedis') 82 | .reply(200, fixture); 83 | 84 | const input$ = ActionsObservable.of(requestCreateEntity({ 85 | body: { 86 | name: 'Yoda', 87 | }, 88 | })); 89 | 90 | return new Promise((resolve, reject) => { 91 | rootEpic(input$, store) 92 | .subscribe((actions: any) => { 93 | try { 94 | expect(actions).toEqual({ 95 | payload: fixture, 96 | type: 'FINISH_CREATE_JEDI', 97 | }); 98 | 99 | resolve(); 100 | } catch (e) { 101 | reject(e); 102 | } 103 | }); 104 | }); 105 | }); 106 | 107 | it('cancels a create entity epic', () => { 108 | const input$ = ActionsObservable.from([ 109 | requestCreateEntity({ 110 | body: { 111 | name: 'Yoda', 112 | }, 113 | }), 114 | cancelCreateEntity(), 115 | ]); 116 | 117 | 118 | return new Promise((resolve, reject) => { 119 | rootEpic(input$, store) 120 | .isEmpty() 121 | .subscribe((res: boolean) => { 122 | try { 123 | expect(res).toBeTruthy(); 124 | resolve(); 125 | } catch (e) { 126 | reject(e); 127 | } 128 | }); 129 | }); 130 | }); 131 | 132 | it('fails to create entity epic', () => { 133 | mockServer 134 | .post('/v1/jedis') 135 | .reply(404); 136 | 137 | const input$ = ActionsObservable.of(requestCreateEntity({ 138 | body: { 139 | name: 'Yoda', 140 | }, 141 | })); 142 | 143 | return new Promise((resolve, reject) => { 144 | rootEpic(input$, store) 145 | .subscribe((actions: any) => { 146 | try { 147 | expect(actions.type).toEqual('FAIL_CREATE_JEDI'); 148 | resolve(); 149 | } catch (e) { 150 | reject(e); 151 | } 152 | }); 153 | }); 154 | }); 155 | }); 156 | 157 | describe('Read Epic', () => { 158 | it('creates a read entity epic with bad params', () => { 159 | const input$ = ActionsObservable.of(requestReadEntity()); 160 | 161 | return new Promise((resolve, reject) => { 162 | rootEpic(input$, store) 163 | .isEmpty() 164 | .subscribe((actions: boolean) => { 165 | try { 166 | expect(actions).toBeTruthy(); 167 | 168 | resolve(); 169 | } catch (e) { 170 | reject(e); 171 | } 172 | }); 173 | }); 174 | }); 175 | 176 | it('creates a read entity epic', () => { 177 | const fixture = { 178 | id: 5, 179 | name: 'Yoda', 180 | }; 181 | 182 | mockServer 183 | .get('/v1/jedis/5') 184 | .reply(200, fixture); 185 | 186 | const input$ = ActionsObservable.of(requestReadEntity({ 187 | id: 5, 188 | })); 189 | 190 | return new Promise((resolve, reject) => { 191 | rootEpic(input$, store) 192 | .subscribe((actions: any) => { 193 | try { 194 | expect(actions).toEqual({ 195 | payload: fixture, 196 | type: 'FINISH_READ_JEDI', 197 | }); 198 | 199 | resolve(); 200 | } catch (e) { 201 | reject(e); 202 | } 203 | }); 204 | }); 205 | }); 206 | 207 | it('cancels a read entity epic', () => { 208 | const input$ = ActionsObservable.from([ 209 | requestReadEntity({ 210 | id: 5, 211 | }), 212 | cancelReadEntity(), 213 | ]); 214 | 215 | 216 | return new Promise((resolve, reject) => { 217 | rootEpic(input$, store) 218 | .isEmpty() 219 | .subscribe((res: boolean) => { 220 | try { 221 | expect(res).toBeTruthy(); 222 | resolve(); 223 | } catch (e) { 224 | reject(e); 225 | } 226 | }); 227 | }); 228 | }); 229 | 230 | it('fails to read entity epic', () => { 231 | mockServer 232 | .get('/v1/jedis/5') 233 | .reply(404); 234 | 235 | const input$ = ActionsObservable.of(requestReadEntity({ 236 | id: 5, 237 | })); 238 | 239 | return new Promise((resolve, reject) => { 240 | rootEpic(input$, store) 241 | .subscribe((actions: any) => { 242 | try { 243 | expect(actions.type).toEqual('FAIL_READ_JEDI'); 244 | resolve(); 245 | } catch (e) { 246 | reject(e); 247 | } 248 | }); 249 | }); 250 | }); 251 | 252 | it('creates a read entities list epic', () => { 253 | const fixture = { 254 | member: [ 255 | { 256 | id: 5, 257 | name: 'Yoda', 258 | }, 259 | { 260 | id: 6, 261 | name: 'Obi Wan', 262 | }, 263 | ], 264 | }; 265 | 266 | mockServer 267 | .get('/v1/jedis') 268 | .reply(200, fixture); 269 | 270 | const input$ = ActionsObservable.of(requestReadEntitiesList()); 271 | 272 | return new Promise((resolve, reject) => { 273 | rootEpic(input$, store) 274 | .subscribe((actions: any) => { 275 | try { 276 | expect(actions).toEqual({ 277 | payload: { 278 | _internalMember: [ 279 | { 280 | id: 5, 281 | name: 'Yoda', 282 | }, 283 | { 284 | id: 6, 285 | name: 'Obi Wan', 286 | }, 287 | ], 288 | member: [ 289 | { 290 | id: 5, 291 | name: 'Yoda', 292 | }, 293 | { 294 | id: 6, 295 | name: 'Obi Wan', 296 | }, 297 | ], 298 | }, 299 | type: 'FINISH_READ_JEDIS_LIST', 300 | }); 301 | 302 | resolve(); 303 | } catch (e) { 304 | reject(e); 305 | } 306 | }); 307 | }); 308 | }); 309 | 310 | it('cancels a read entities list epic', () => { 311 | const input$ = ActionsObservable.from([ 312 | requestReadEntitiesList(), 313 | cancelReadEntitiesList(), 314 | ]); 315 | 316 | return new Promise((resolve, reject) => { 317 | rootEpic(input$, store) 318 | .isEmpty() 319 | .subscribe((res: boolean) => { 320 | try { 321 | expect(res).toBeTruthy(); 322 | resolve(); 323 | } catch (e) { 324 | reject(e); 325 | } 326 | }); 327 | }); 328 | }); 329 | 330 | it('fails to read entity epic', () => { 331 | mockServer 332 | .get('/v1/jedis') 333 | .reply(404); 334 | 335 | const input$ = ActionsObservable.of(requestReadEntitiesList()); 336 | 337 | return new Promise((resolve, reject) => { 338 | rootEpic(input$, store) 339 | .subscribe((actions: any) => { 340 | try { 341 | expect(actions.type).toEqual('FAIL_READ_JEDIS_LIST'); 342 | resolve(); 343 | } catch (e) { 344 | reject(e); 345 | } 346 | }); 347 | }); 348 | }); 349 | 350 | it('creates a read batch entities epic with bad params', () => { 351 | const input$ = ActionsObservable.of(requestReadBatchEntities()); 352 | 353 | return new Promise((resolve, reject) => { 354 | rootEpic(input$, store) 355 | .isEmpty() 356 | .subscribe((actions: boolean) => { 357 | try { 358 | expect(actions).toBeTruthy(); 359 | 360 | resolve(); 361 | } catch (e) { 362 | reject(e); 363 | } 364 | }); 365 | }); 366 | }); 367 | 368 | it('creates a read batch entities epic', () => { 369 | const fixture = [ 370 | { 371 | id: 5, 372 | name: 'Yoda', 373 | }, 374 | { 375 | id: 6, 376 | name: 'Obi Wan', 377 | }, 378 | ]; 379 | 380 | mockServer 381 | .get('/v1/jedis/5') 382 | .reply(200, fixture[0]) 383 | .get('/v1/jedis/6') 384 | .reply(200, fixture[1]); 385 | 386 | const input$ = ActionsObservable.of(requestReadBatchEntities({ 387 | ids: [5, 6], 388 | })); 389 | 390 | return new Promise((resolve, reject) => { 391 | rootEpic(input$, store) 392 | .subscribe((actions: any) => { 393 | try { 394 | expect(actions).toEqual({ 395 | payload: fixture, 396 | type: 'FINISH_READ_BATCH_JEDIS', 397 | }); 398 | 399 | resolve(); 400 | } catch (e) { 401 | reject(e); 402 | } 403 | }); 404 | }); 405 | }); 406 | 407 | it('cancels a read batch entities epic', () => { 408 | const input$ = ActionsObservable.from([ 409 | requestReadBatchEntities({ 410 | ids: [5, 6], 411 | }), 412 | cancelReadBatchEntities(), 413 | ]); 414 | 415 | return new Promise((resolve, reject) => { 416 | rootEpic(input$, store) 417 | .isEmpty() 418 | .subscribe((res: boolean) => { 419 | try { 420 | expect(res).toBeTruthy(); 421 | resolve(); 422 | } catch (e) { 423 | reject(e); 424 | } 425 | }); 426 | }); 427 | }); 428 | 429 | it('fails to read batch of entities epic', () => { 430 | mockServer 431 | .get('/v1/jedis/5') 432 | .reply(404) 433 | .get('/v1/jedis/6') 434 | .reply(404); 435 | 436 | const input$ = ActionsObservable.of(requestReadBatchEntities({ 437 | ids: [5, 6], 438 | })); 439 | 440 | return new Promise((resolve, reject) => { 441 | rootEpic(input$, store) 442 | .subscribe((actions: any) => { 443 | try { 444 | expect(actions.type).toEqual('FAIL_READ_BATCH_JEDIS'); 445 | resolve(); 446 | } catch (e) { 447 | reject(e); 448 | } 449 | }); 450 | }); 451 | }); 452 | }); 453 | 454 | describe('Update Epic', () => { 455 | it('creates a update entity epic with bad params', () => { 456 | const input$ = ActionsObservable.of(requestUpdateEntity()); 457 | 458 | return new Promise((resolve, reject) => { 459 | rootEpic(input$, store) 460 | .isEmpty() 461 | .subscribe((actions: boolean) => { 462 | try { 463 | expect(actions).toBeTruthy(); 464 | 465 | resolve(); 466 | } catch (e) { 467 | reject(e); 468 | } 469 | }); 470 | }); 471 | }); 472 | 473 | it('creates a update entity epic', () => { 474 | const fixture = { 475 | id: 5, 476 | name: 'Yoda', 477 | }; 478 | 479 | mockServer 480 | .put('/v1/jedis/5') 481 | .reply(200, fixture); 482 | 483 | const input$ = ActionsObservable.of(requestUpdateEntity({ 484 | body: { 485 | name: 'Yoda', 486 | }, 487 | id: 5, 488 | })); 489 | 490 | return new Promise((resolve, reject) => { 491 | rootEpic(input$, store) 492 | .subscribe((actions: any) => { 493 | try { 494 | expect(actions).toEqual({ 495 | payload: fixture, 496 | type: 'FINISH_UPDATE_JEDI', 497 | }); 498 | 499 | resolve(); 500 | } catch (e) { 501 | reject(e); 502 | } 503 | }); 504 | }); 505 | }); 506 | 507 | it('cancels a update entity epic', () => { 508 | const input$ = ActionsObservable.from([ 509 | requestUpdateEntity({ 510 | body: { 511 | name: 'Yoda', 512 | }, 513 | id: 5, 514 | }), 515 | cancelUpdateEntity(), 516 | ]); 517 | 518 | 519 | return new Promise((resolve, reject) => { 520 | rootEpic(input$, store) 521 | .isEmpty() 522 | .subscribe((res: boolean) => { 523 | try { 524 | expect(res).toBeTruthy(); 525 | resolve(); 526 | } catch (e) { 527 | reject(e); 528 | } 529 | }); 530 | }); 531 | }); 532 | 533 | it('fails to update entity epic', () => { 534 | mockServer 535 | .put('/v1/jedis/5') 536 | .reply(404); 537 | 538 | const input$ = ActionsObservable.of(requestUpdateEntity({ 539 | body: { 540 | name: 'Yoda', 541 | }, 542 | id: 5, 543 | })); 544 | 545 | return new Promise((resolve, reject) => { 546 | rootEpic(input$, store) 547 | .subscribe((actions: any) => { 548 | try { 549 | expect(actions.type).toEqual('FAIL_UPDATE_JEDI'); 550 | resolve(); 551 | } catch (e) { 552 | reject(e); 553 | } 554 | }); 555 | }); 556 | }); 557 | }); 558 | 559 | describe('Delete Epic', () => { 560 | it('creates a delete entity epic with bad params', () => { 561 | const input$ = ActionsObservable.of(requestDeleteEntity()); 562 | 563 | return new Promise((resolve, reject) => { 564 | rootEpic(input$, store) 565 | .isEmpty() 566 | .subscribe((actions: boolean) => { 567 | try { 568 | expect(actions).toBeTruthy(); 569 | 570 | resolve(); 571 | } catch (e) { 572 | reject(e); 573 | } 574 | }); 575 | }); 576 | }); 577 | 578 | it('creates a delete entity epic', () => { 579 | mockServer 580 | .delete('/v1/jedis/5') 581 | .reply(201); 582 | 583 | const input$ = ActionsObservable.of(requestDeleteEntity({ 584 | id: 5, 585 | })); 586 | 587 | return new Promise((resolve, reject) => { 588 | rootEpic(input$, store) 589 | .subscribe((actions: any) => { 590 | try { 591 | expect(actions).toEqual({ 592 | payload: 5, 593 | type: 'FINISH_DELETE_JEDI', 594 | }); 595 | 596 | resolve(); 597 | } catch (e) { 598 | reject(e); 599 | } 600 | }); 601 | }); 602 | }); 603 | 604 | it('cancels a delete entity epic', () => { 605 | const input$ = ActionsObservable.from([ 606 | requestDeleteEntity({ 607 | id: 5, 608 | }), 609 | cancelDeleteEntity(), 610 | ]); 611 | 612 | 613 | return new Promise((resolve, reject) => { 614 | rootEpic(input$, store) 615 | .isEmpty() 616 | .subscribe((res: boolean) => { 617 | try { 618 | expect(res).toBeTruthy(); 619 | resolve(); 620 | } catch (e) { 621 | reject(e); 622 | } 623 | }); 624 | }); 625 | }); 626 | 627 | it('fails to delete entity epic', () => { 628 | mockServer 629 | .delete('/v1/jedis/5') 630 | .reply(404); 631 | 632 | const input$ = ActionsObservable.of(requestDeleteEntity({ 633 | id: 5, 634 | })); 635 | 636 | return new Promise((resolve, reject) => { 637 | rootEpic(input$, store) 638 | .subscribe((actions: any) => { 639 | try { 640 | expect(actions.type).toEqual('FAIL_DELETE_JEDI'); 641 | resolve(); 642 | } catch (e) { 643 | reject(e); 644 | } 645 | }); 646 | }); 647 | }); 648 | 649 | it('creates a delete a batch of entities epic with bad params', () => { 650 | const input$ = ActionsObservable.of(requestDeleteBatchEntities()); 651 | 652 | return new Promise((resolve, reject) => { 653 | rootEpic(input$, store) 654 | .isEmpty() 655 | .subscribe((actions: boolean) => { 656 | try { 657 | expect(actions).toBeTruthy(); 658 | 659 | resolve(); 660 | } catch (e) { 661 | reject(e); 662 | } 663 | }); 664 | }); 665 | }); 666 | 667 | it('creates a delete batch entities epic', () => { 668 | mockServer 669 | .delete('/v1/jedis/5') 670 | .reply(201) 671 | .delete('/v1/jedis/6') 672 | .reply(201); 673 | 674 | const input$ = ActionsObservable.of(requestDeleteBatchEntities({ 675 | ids: [5, 6], 676 | })); 677 | 678 | return new Promise((resolve, reject) => { 679 | rootEpic(input$, store) 680 | .subscribe((actions: any) => { 681 | try { 682 | expect(actions).toEqual({ 683 | payload: [5, 6], 684 | type: 'FINISH_DELETE_BATCH_JEDIS', 685 | }); 686 | 687 | resolve(); 688 | } catch (e) { 689 | reject(e); 690 | } 691 | }); 692 | }); 693 | }); 694 | 695 | it('cancels a delete batch entities epic', () => { 696 | const input$ = ActionsObservable.from([ 697 | requestDeleteBatchEntities({ 698 | ids: [5, 6], 699 | }), 700 | cancelDeleteBatchEntities(), 701 | ]); 702 | 703 | return new Promise((resolve, reject) => { 704 | rootEpic(input$, store) 705 | .isEmpty() 706 | .subscribe((res: boolean) => { 707 | try { 708 | expect(res).toBeTruthy(); 709 | resolve(); 710 | } catch (e) { 711 | reject(e); 712 | } 713 | }); 714 | }); 715 | }); 716 | 717 | it('fails to delete batch of entities epic', () => { 718 | mockServer 719 | .delete('/v1/jedis/5') 720 | .reply(404) 721 | .delete('/v1/jedis/6') 722 | .reply(404); 723 | 724 | const input$ = ActionsObservable.of(requestDeleteBatchEntities({ 725 | ids: [5, 6], 726 | })); 727 | 728 | return new Promise((resolve, reject) => { 729 | rootEpic(input$, store) 730 | .subscribe((actions: any) => { 731 | try { 732 | expect(actions.type).toEqual('FAIL_DELETE_BATCH_JEDIS'); 733 | resolve(); 734 | } catch (e) { 735 | reject(e); 736 | } 737 | }); 738 | }); 739 | }); 740 | }); 741 | }); 742 | --------------------------------------------------------------------------------