├── .gitignore ├── README.md ├── action-types.js ├── actions.js ├── constants.js ├── epic.js ├── example.js ├── index.js ├── package.json ├── test ├── action-types.js ├── actions.js ├── app.js ├── epic.js ├── index.js └── updater.js └── updater.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | npm-debug.log* 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # feathers-action 2 | 3 | > never write another CRUD redux action! 4 | 5 | this module helps you use [`feathers`](http://feathersjs.com), [`redux`](http://redux.js.org), and [`tcomb`](https://www.npmjs.com/package/tcomb). 6 | 7 | ## Usage 8 | 9 | ```js 10 | const createModule = require('feathers-action') 11 | const createCid = require('cuid') 12 | 13 | const module = createModule('cats') 14 | // module.actions 15 | // module.epic 16 | // module.updater 17 | ``` 18 | 19 | ## Dependencies 20 | 21 | - [`feathers-reactive`](https://redux-observable.js.org/): must add plugin to feathers app / client 22 | - [`redux-observable`](https://redux-observable.js.org/): must add middleware to redux store 23 | 24 | ## API 25 | 26 | ### module = feathersAction(name) 27 | ### module = feathersAction(options) 28 | 29 | `options`: 30 | 31 | - `name` 32 | - `methods` 33 | - TODO `idField` 34 | 35 | ### { actions, updater, epic } = module 36 | 37 | - `actions`: object where keys are method names (`find`, `get`, `create`, ...) 38 | - [`updater`](https://github.com/rvikmanis/redux-fp): `action => state => nextState` 39 | - [`epic`](https://redux-observable.js.org/docs/basics/Epics.html): `(action, action$, store) => nextAction$` 40 | 41 | ### modules = feathersAction([name, ...]) 42 | ### modules = feathersAction([options, ...]) 43 | 44 | where `modules` is an object where key is `name` and value is `module` as above. 45 | 46 | ### methodAction = module.actions[method](cid, ...args) 47 | 48 | each action creator receives a `cid` (client-generated id) as the first argument. 49 | 50 | all subsequent arguments for feathers methods are the same as the corresponding methods on the [feathers service](https://docs.feathersjs.com/api/services.html). 51 | 52 | #### completeAction = module.actions.complete(cid) 53 | 54 | cancels a long-running subscription as in `find` or `get`. 55 | 56 | #### errorAction = module.actions.error(cid, err) 57 | 58 | #### setAction = module.actions.set(cid, key, value) 59 | 60 | sets the given key as value in the corresponding redux state. 61 | 62 | to unset (remove key), value is `undefined`. 63 | 64 | ### nextState = module.updater(action)(state) 65 | 66 | see ["updater" in `redux-fp`](https://github.com/rvikmanis/redux-fp): `action => state => nextState` 67 | 68 | ### nextAction$ = module.epic(action$, store, { feathers }) 69 | 70 | see ["epic" in `redux-observable`](https://redux-observable.js.org/docs/basics/Epics.html): `(action, action$, store) => nextAction$` 71 | 72 | must pass in `{ feathers }` as `deps` to [`createEpicMiddleware`](https://redux-observable.js.org/docs/basics/SettingUpTheMiddleware.html) 73 | 74 | ```js 75 | // client 76 | const Feathers = require('feathers/client') 77 | const feathersSockets = require('feathers-socketio/client') 78 | const feathersRx = require('feathers-reactive') 79 | const Rx = require('rxjs') 80 | 81 | const socket = io() 82 | const feathers = Feathers() 83 | .configure(feathersSockets(socket)) 84 | .configure(feathersRx(Rx)) 85 | 86 | // store 87 | const { createStore, applyMiddleware } = require('redux') 88 | const { createEpicMiddleware } = require('redux-observable') 89 | 90 | const rootEpic = require('./epic') 91 | const rootUpdater = require('./updater') 92 | 93 | const epicMiddleware = createEpicMiddleware(rootEpic, { 94 | dependencies: { feathers } 95 | }) 96 | 97 | const store = createStore( 98 | (state, action) => rootUpdater(action)(state), 99 | applyMiddleware(epicMiddleware) 100 | ) 101 | ``` 102 | 103 | ## Install 104 | 105 | With [npm](https://npmjs.org/) installed, run 106 | 107 | ``` 108 | $ npm install feathers-action --save 109 | ``` 110 | 111 | ## Acknowledgments 112 | 113 | feathers-action was inspired by.. 114 | 115 | ## See Also 116 | 117 | - redux 118 | - feathers 119 | 120 | ## License 121 | 122 | The Apache License 123 | 124 | Copyright © 2017 Michael Williams 125 | 126 | Licensed under the Apache License, Version 2.0 (the "License"); 127 | you may not use this file except in compliance with the License. 128 | You may obtain a copy of the License at 129 | 130 | http://www.apache.org/licenses/LICENSE-2.0 131 | 132 | Unless required by applicable law or agreed to in writing, software 133 | distributed under the License is distributed on an "AS IS" BASIS, 134 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 135 | See the License for the specific language governing permissions and 136 | limitations under the License. 137 | -------------------------------------------------------------------------------- /action-types.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const pipe = require('ramda/src/pipe') 4 | const merge = require('ramda/src/merge') 5 | const invertObj = require('ramda/src/invertObj') 6 | const prepend = require('ramda/src/prepend') 7 | const toUpper = require('ramda/src/toUpper') 8 | const map = require('ramda/src/map') 9 | const mapObjIndexed = require('ramda/src/mapObjIndexed') 10 | const join = require('ramda/src/join') 11 | 12 | const { FEATHERS, DEFAULT_METHODS } = require('./constants') 13 | 14 | module.exports = createActionTypes 15 | 16 | function createActionTypes (options) { 17 | const { 18 | service, 19 | methods = DEFAULT_METHODS 20 | } = options 21 | 22 | const createActionType = ActionType(service) 23 | 24 | return merge( 25 | getActionTypesForMethods(createActionType, methods), 26 | { 27 | set: createActionType(['set']), 28 | setAll: createActionType(['setAll']), 29 | unset: createActionType(['unset']), 30 | unsetAll: createActionType(['unsetAll']), 31 | start: createActionType(['start']), 32 | ready: createActionType(['ready']), 33 | complete: createActionType(['complete']), 34 | error: createActionType(['error']) 35 | } 36 | ) 37 | } 38 | 39 | const ActionType = (service) => { 40 | return pipe( 41 | prepend(service), 42 | prepend(FEATHERS), 43 | map(toUpper), 44 | join('_') 45 | ) 46 | } 47 | 48 | const getActionTypesForMethods = (createActionType, methods) => { 49 | return pipe( 50 | invertObj, 51 | mapObjIndexed((_, method) => createActionType([method])) 52 | )(methods) 53 | } 54 | -------------------------------------------------------------------------------- /actions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const createAction = require('@f/create-action') 4 | const pipe = require('ramda/src/pipe') 5 | const mapObjIndexed = require('ramda/src/mapObjIndexed') 6 | const merge = require('ramda/src/merge') 7 | const invertObj = require('ramda/src/invertObj') 8 | 9 | const createActionTypes = require('./action-types') 10 | 11 | module.exports = createActionCreators 12 | 13 | function createActionCreators (options) { 14 | const actionTypes = createActionTypes(options) 15 | 16 | const getActionCreatorsForTypes = mapObjIndexed((_, type) => { 17 | return ActionCreator(type) 18 | }) 19 | 20 | return getActionCreatorsForTypes(actionTypes) 21 | 22 | function ActionCreator (type) { 23 | const argsCreator = argsCreatorByType[type] 24 | const payloadCreator = (cid, ...args) => argsCreator(...args) 25 | const actionCreator = createAction(actionTypes[type], payloadCreator, metaCreator) 26 | return actionCreator 27 | } 28 | } 29 | 30 | const argsCreatorByType = { 31 | find: (params = {}) => ({ params }), 32 | get: (id, params = {}) => ({ id, params }), 33 | create: (data, params = {}) => ({ data, params }), 34 | update: (id, data, params = {}) => ({ id, data, params }), 35 | patch: (id, data, params = {}) => ({ id, data, params }), 36 | remove: (id, params = {}) => ({ id, params }), 37 | 38 | set: (id, data) => ({ id, data }), 39 | unset: (id, data) => ({ id, data }), 40 | setAll: (data) => data, 41 | unsetAll: (data) => data, 42 | 43 | start: (request) => request, 44 | ready: () => null, 45 | complete: (result) => result, 46 | error: (err) => err 47 | } 48 | 49 | function payloadCreator (type) { 50 | const argsCreator = argsCreatorByMethod[type] 51 | return (cid, ...args) => argsCreator(...args) 52 | } 53 | 54 | function metaCreator (cid) { 55 | return { cid } 56 | } 57 | -------------------------------------------------------------------------------- /constants.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const FEATHERS = 'FEATHERS' 4 | 5 | const DEFAULT_ID_FIELD = 'id' 6 | const DEFAULT_METHODS = ['find', 'get', 'create', 'update', 'patch', 'remove'] 7 | 8 | module.exports = { 9 | FEATHERS, 10 | DEFAULT_ID_FIELD, 11 | DEFAULT_METHODS 12 | } 13 | -------------------------------------------------------------------------------- /epic.js: -------------------------------------------------------------------------------- 1 | const { combineEpics } = require('redux-observable') 2 | const is = require('typeof-is') 3 | const either = require('ramda/src/either') 4 | const merge = require('ramda/src/merge') 5 | const map = require('ramda/src/map') 6 | const __ = require('ramda/src/__') 7 | const mapObjIndexed = require('ramda/src/mapObjIndexed') 8 | const values = require('ramda/src/values') 9 | const pathEq = require('ramda/src/pathEq') 10 | const propEq = require('ramda/src/propEq') 11 | const find = require('ramda/src/find') 12 | const not = require('ramda/src/not') 13 | const filter = require('ramda/src/filter') 14 | // TODO split into individual modules 15 | const Rx = require('rxjs/Rx') 16 | 17 | const createActionTypes = require('./action-types') 18 | const createActionCreators = require('./actions') 19 | 20 | module.exports = createEpic 21 | 22 | function createEpic (options) { 23 | const { 24 | service 25 | } = options 26 | 27 | const actionTypes = createActionTypes(options) 28 | const actionCreators = createActionCreators(options) 29 | 30 | const epics = createEpics({ actionTypes, actionCreators, service }) 31 | return combineEpics(...values(epics)) 32 | } 33 | 34 | const requestArgs = { 35 | find: ({ params }) => [params], 36 | get: ({ id, params }) => [id, params], 37 | create: ({ data, params }) => [data, params], 38 | update: ({ id, data, params }) => [id, data, params], 39 | patch: ({ id, data, params }) => [id, data, params], 40 | remove: ({ id, params }) => [id, params] 41 | } 42 | 43 | const createRequestHandlers = actions => { 44 | return { 45 | find: (response$, { cid }) => Rx.Observable 46 | .concat( 47 | // set all initial results 48 | response$.first().concatMap(values => Rx.Observable.of( 49 | actions.setAll(cid, values), 50 | actions.ready(cid) 51 | )), 52 | // update the next results as a pairwise diff 53 | response$.pairwise().concatMap(([prev, next]) => { 54 | const removed = getRemoved(prev, next) 55 | const diff = [ 56 | actions.unsetAll(cid, removed), 57 | actions.setAll(cid, next) 58 | ] 59 | return Rx.Observable.of(...diff) 60 | }) 61 | ), 62 | get: (response$, { cid, args }) => Rx.Observable 63 | .merge( 64 | response$ 65 | .map(value => { 66 | // `feathers-reactive` return null on 'removed' events 67 | return (value === null) 68 | ? actions.unset(cid, args.id) 69 | : actions.set(cid, args.id, value) 70 | }), 71 | response$.first().mapTo(actions.ready(cid)) 72 | ), 73 | create: (response$, { cid, args }) => { 74 | const setOptimistic = actions.set(cid, cid, args.data) 75 | const unsetOptimistic = actions.unset(cid, cid) 76 | 77 | const responseAction$ = response$ 78 | .take(1) 79 | .concatMap(value => Rx.Observable.of( 80 | actions.set(cid, value.id, value), 81 | actions.ready(cid) 82 | )) 83 | .catch(err => Rx.Observable.of(actions.error(cid, err))) 84 | 85 | return Rx.Observable.of(setOptimistic) 86 | .concat(responseAction$.startWith(unsetOptimistic)) 87 | }, 88 | update: createUpdateOrPatchHandler({ 89 | method: 'update', 90 | getOptimisticData: ({ args }) => { 91 | return merge({ id: args.id }, args.data) 92 | } 93 | }), 94 | patch: createUpdateOrPatchHandler({ 95 | method: 'patch', 96 | getOptimisticData: ({ args, previousData }) => { 97 | return merge(previousData, args.data) 98 | } 99 | }), 100 | remove: (response$, { cid, args, service, store }) => { 101 | const state = store.getState() 102 | const previousData = state[service][args.id] 103 | const setOptimistic = actions.unset(cid, args.id) 104 | const resetOptimistic = actions.set(cid, args.id, previousData) 105 | 106 | const responseAction$ = response$ 107 | .take(1) 108 | .concatMap(value => Rx.Observable.of( 109 | actions.unset(cid, value.id), 110 | actions.ready(cid) 111 | )) 112 | .catch(err => Rx.Observable.of(resetOptimistic, actions.error(cid, err))) 113 | 114 | return Rx.Observable.of(setOptimistic) 115 | .concat(responseAction$) 116 | } 117 | } 118 | 119 | // TODO: only rollback when _all_ updates for that id have errored 120 | // TODO: find a way to pass in actions for all updates of the same id 121 | function createUpdateOrPatchHandler (options) { 122 | const { method, getOptimisticData } = options 123 | 124 | return (response$, { cid, args, service, store }) => { 125 | const state = store.getState() 126 | const previousData = state[service][args.id] 127 | const optimisticData = getOptimisticData({ args, previousData }) 128 | 129 | const setOptimistic = actions.set(cid, args.id, optimisticData) 130 | const resetOptimistic = actions.set(cid, args.id, previousData) 131 | 132 | const responseAction$ = response$ 133 | .take(1) 134 | .concatMap(value => Rx.Observable.of( 135 | actions.set(cid, value.id, value), 136 | actions.ready(cid) 137 | )) 138 | .catch(err => Rx.Observable.of(resetOptimistic, actions.error(cid, err))) 139 | 140 | return Rx.Observable.of(setOptimistic) 141 | .concat(responseAction$) 142 | } 143 | } 144 | } 145 | 146 | const createEpics = ({ actionTypes, actionCreators, service }) => { 147 | const isCompleteAction = isType(actionTypes.complete) 148 | const isErrorAction = isType(actionTypes.error) 149 | const isEndAction = either(isCompleteAction, isErrorAction) 150 | const requestHandlers = createRequestHandlers(actionCreators) 151 | const mapRequestHandlers = mapObjIndexed((requestHandler, method) => { 152 | return (action$, store, deps) => { 153 | assertFeathersDep(deps) 154 | 155 | const { feathers } = deps 156 | 157 | const requester = createRequester({ method, feathers, service }) 158 | 159 | return action$.ofType(actionTypes[method]) 160 | .mergeMap(action => { 161 | const args = action.payload 162 | const { cid } = action.meta 163 | 164 | const response$ = requester(args) 165 | const requestAction$ = requestHandler(response$, { cid, args, service, store }) 166 | 167 | const cidAction$ = action$.filter(isCid(cid)) 168 | const completeAction$ = cidAction$.filter(isCompleteAction) 169 | const errorAction$ = cidAction$.filter(isErrorAction) 170 | const cancelAction$ = completeAction$.merge(errorAction$) 171 | 172 | return Rx.Observable.of(actionCreators.start(cid, { service, method, args })) 173 | .concat(requestAction$) 174 | .concat(Rx.Observable.of(actionCreators.complete(cid))) 175 | .catch(err => Rx.Observable.of(actionCreators.error(cid, err))) 176 | .takeUntil(cancelAction$) 177 | .filter(endOnce(isEndAction)) 178 | }) 179 | } 180 | }) 181 | return mapRequestHandlers(requestHandlers) 182 | } 183 | 184 | const createRequester = ({ feathers, method, service }) => { 185 | const feathersService = feathers.service(service) 186 | return payload => { 187 | const args = requestArgs[method](payload) 188 | return feathersService[method](...args) 189 | } 190 | } 191 | 192 | function assertFeathersDep (deps = {}) { 193 | if (is.undefined(deps.feathers)) { 194 | throw new Error('feathers-action/epic: expected feathers app or client to be given as dependency in redux-observable middleware') 195 | } 196 | } 197 | 198 | const isCid = pathEq(['meta', 'cid']) 199 | const isType = propEq('type') 200 | 201 | const endOnce = (isEndAction) => { 202 | var isDone = false 203 | return (value) => { 204 | if (isEndAction(value)) { 205 | if (isDone) return false 206 | isDone = true 207 | return true 208 | } 209 | return true 210 | } 211 | } 212 | 213 | const getRemoved = (prev, next) => { 214 | return filter(value => { 215 | return not(find(propEq('id', value.id), next)) 216 | }, prev) 217 | } 218 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | const createModule = require('./') 2 | 3 | const cats = createModule('cats') 4 | 5 | console.log('cats', cats) 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { isArray } = Array 4 | const is = require('typeof-is') 5 | const pipe = require('ramda/src/pipe') 6 | const indexBy = require('ramda/src/indexBy') 7 | const prop = require('ramda/src/prop') 8 | const map = require('ramda/src/map') 9 | 10 | const createActions = require('./actions') 11 | const createUpdater = require('./updater') 12 | const createEpic = require('./epic') 13 | 14 | module.exports = createModule 15 | 16 | function createModule (options = {}) { 17 | if (is.string(options)) { 18 | options = { service: options } 19 | } 20 | 21 | if (isArray(options)) { 22 | return createModules(options) 23 | } 24 | 25 | const { service } = options 26 | 27 | return { 28 | name: service, 29 | actions: createActions(options), 30 | updater: createUpdater(options), 31 | epic: createEpic(options) 32 | } 33 | } 34 | 35 | const createModules = pipe( 36 | map(createModule), 37 | indexBy(prop('name')) 38 | ) 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feathers-action", 3 | "version": "2.4.0", 4 | "description": "use feathers services with redux", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node-dev example", 8 | "test:deps": "dependency-check . && dependency-check . --extra --no-dev -i es2040", 9 | "test:lint": "standard", 10 | "test:node": "NODE_ENV=test run-default tape test/*.js --", 11 | "test:coverage": "NODE_ENV=test nyc npm run test:node", 12 | "test:coverage:report": "nyc report --reporter=lcov npm run test:node", 13 | "test": "npm-run-all -s test:node test:lint test:deps" 14 | }, 15 | "browserify": { 16 | "transform": [ 17 | "es2040" 18 | ] 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/ahdinosaur/feathers-action.git" 23 | }, 24 | "keywords": [ 25 | "feathers", 26 | "redux", 27 | "rest", 28 | "crud", 29 | "api", 30 | "frp", 31 | "flux" 32 | ], 33 | "author": "Mikey (http://dinosaur.is)", 34 | "license": "Apache-2.0", 35 | "bugs": { 36 | "url": "https://github.com/ahdinosaur/feathers-action/issues" 37 | }, 38 | "homepage": "https://github.com/ahdinosaur/feathers-action#readme", 39 | "devDependencies": { 40 | "cuid": "^1.3.8", 41 | "deep-freeze": "0.0.1", 42 | "dependency-check": "^2.7.0", 43 | "feathers": "^2.1.1", 44 | "feathers-memory": "^1.1.0", 45 | "feathers-reactive": "^0.4.1", 46 | "node-dev": "^3.1.3", 47 | "npm-run-all": "^4.0.1", 48 | "nyc": "^10.1.2", 49 | "redux": "^3.6.0", 50 | "redux-observable": "^0.14.1", 51 | "run-default": "^1.0.0", 52 | "standard": "^10.0.1", 53 | "tape": "^4.6.3" 54 | }, 55 | "dependencies": { 56 | "@f/create-action": "^1.1.1", 57 | "es2040": "^1.2.5", 58 | "ramda": "^0.23.0", 59 | "redux-fp": "^0.2.0", 60 | "redux-observable": "^0.14.1", 61 | "rxjs": "^5.3.0", 62 | "typeof-is": "^1.0.2" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test/action-types.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | 3 | const createActionTypes = require('../action-types') 4 | 5 | test('returns the service action ids', function (t) { 6 | const actionTypes = createActionTypes({ service: 'cats' }) 7 | 8 | t.equal(actionTypes.find, 'FEATHERS_CATS_FIND') 9 | t.equal(actionTypes.get, 'FEATHERS_CATS_GET') 10 | t.equal(actionTypes.create, 'FEATHERS_CATS_CREATE') 11 | t.equal(actionTypes.update, 'FEATHERS_CATS_UPDATE') 12 | t.equal(actionTypes.patch, 'FEATHERS_CATS_PATCH') 13 | t.equal(actionTypes.remove, 'FEATHERS_CATS_REMOVE') 14 | 15 | t.equal(actionTypes.set, 'FEATHERS_CATS_SET') 16 | 17 | t.equal(actionTypes.start, 'FEATHERS_CATS_START') 18 | t.equal(actionTypes.complete, 'FEATHERS_CATS_COMPLETE') 19 | t.equal(actionTypes.error, 'FEATHERS_CATS_ERROR') 20 | 21 | t.end() 22 | }) 23 | -------------------------------------------------------------------------------- /test/actions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | 5 | const { all, has, __ } = require('ramda') 6 | 7 | const createModule = require('../') 8 | const createActionTypes = require('../action-types') 9 | 10 | const cats = createModule('cats') 11 | const actionTypes = createActionTypes({ service: 'cats' }) 12 | 13 | test('action creators have correct keys', function (t) { 14 | const keys = ['find', 'create', 'get', 'update', 'patch', 'remove', 'set', 'start', 'complete', 'error'] 15 | const hasActions = has(__, cats.actions) 16 | const hasAllActions = all(hasActions) 17 | t.true(hasAllActions(keys)) 18 | t.end() 19 | }) 20 | 21 | test('request start', function (t) { 22 | const cid = 'abcd' 23 | const expectedAction = { 24 | type: actionTypes.start, 25 | payload: { 26 | service: 'cats', 27 | method: 'create', 28 | args: { 29 | data: { 30 | name: 'fluffy' 31 | } 32 | } 33 | }, 34 | meta: { 35 | cid 36 | } 37 | } 38 | 39 | const action = cats.actions.start(cid, { 40 | service: 'cats', 41 | method: 'create', 42 | args: { 43 | data: { 44 | name: 'fluffy' 45 | } 46 | } 47 | }) 48 | 49 | t.deepEqual(action, expectedAction) 50 | t.end() 51 | }) 52 | 53 | test('request success', function (t) { 54 | const cid = 'abcd' 55 | const result = { 56 | id: 1, 57 | name: 'fluffy' 58 | } 59 | const expectedAction = { 60 | type: actionTypes.complete, 61 | payload: result, 62 | meta: { 63 | cid 64 | } 65 | } 66 | 67 | const action = cats.actions.complete(cid, result) 68 | 69 | t.deepEqual(action, expectedAction) 70 | t.end() 71 | }) 72 | 73 | test('request error', function (t) { 74 | const cid = 'abcd' 75 | const err = new Error('request failed, meow.') 76 | const expectedAction = { 77 | type: actionTypes.error, 78 | payload: err, 79 | meta: { 80 | cid 81 | } 82 | } 83 | 84 | const action = cats.actions.error(cid, err) 85 | 86 | t.deepEqual(action, expectedAction) 87 | t.end() 88 | }) 89 | 90 | test('find returns the correct action', function (t) { 91 | const cid = 'abcd' 92 | const findAction = cats.actions.find(cid, {}) 93 | const expectedAction = { 94 | type: actionTypes.find, 95 | payload: { 96 | params: {} 97 | }, 98 | meta: { 99 | cid 100 | } 101 | } 102 | t.deepEqual(findAction, expectedAction) 103 | t.end() 104 | }) 105 | 106 | test('get returns the correct action', function (t) { 107 | const cid = 'abcd' 108 | const getAction = cats.actions.get(cid, 1) 109 | const expectedAction = { 110 | type: actionTypes.get, 111 | payload: { 112 | id: 1, 113 | params: {} 114 | }, 115 | meta: { 116 | cid 117 | } 118 | } 119 | t.deepEqual(getAction, expectedAction) 120 | t.end() 121 | }) 122 | 123 | test('create returns the correct action', function (t) { 124 | const cid = 'abcd' 125 | const createAction = cats.actions.create(cid, { name: 'fluffy' }) 126 | const expectedAction = { 127 | type: actionTypes.create, 128 | payload: { 129 | data: { 130 | name: 'fluffy' 131 | }, 132 | params: {} 133 | }, 134 | meta: { 135 | cid 136 | } 137 | } 138 | t.deepEqual(createAction, expectedAction) 139 | t.end() 140 | }) 141 | 142 | test('update returns the correct action', function (t) { 143 | const cid = 'abcd' 144 | const updateAction = cats.actions.update(cid, 1, { id: 1, name: 'fluffy' }) 145 | const expectedAction = { 146 | type: actionTypes.update, 147 | payload: { 148 | id: 1, 149 | data: { 150 | id: 1, 151 | name: 'fluffy' 152 | }, 153 | params: {} 154 | }, 155 | meta: { 156 | cid 157 | } 158 | } 159 | t.deepEqual(updateAction, expectedAction) 160 | t.end() 161 | }) 162 | 163 | test('patch returns the correct action', function (t) { 164 | const cid = 'abcd' 165 | const patchAction = cats.actions.patch(cid, 1, { name: 'fluffy' }) 166 | const expectedAction = { 167 | type: actionTypes.patch, 168 | payload: { 169 | id: 1, 170 | data: { 171 | name: 'fluffy' 172 | }, 173 | params: {} 174 | }, 175 | meta: { 176 | cid 177 | } 178 | } 179 | t.deepEqual(patchAction, expectedAction) 180 | t.end() 181 | }) 182 | 183 | test('remove returns the correct action', function (t) { 184 | const cid = 'abcd' 185 | const removeAction = cats.actions.remove(cid, 1) 186 | const expectedAction = { 187 | type: actionTypes.remove, 188 | payload: { 189 | id: 1, 190 | params: {} 191 | }, 192 | meta: { 193 | cid 194 | } 195 | } 196 | t.deepEqual(removeAction, expectedAction) 197 | t.end() 198 | }) 199 | -------------------------------------------------------------------------------- /test/app.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var feathers = require('feathers') 3 | var memory = require('feathers-memory') 4 | var {createStore, applyMiddleware} = require('redux') 5 | var { createEpicMiddleware, combineEpics } = require('redux-observable') 6 | var feathersReactive = require('feathers-reactive') 7 | var rxjs = require('rxjs') 8 | var reduxFp = require('redux-fp') 9 | var Cid = require('cuid') 10 | 11 | var createModule = require('../') 12 | 13 | const serviceNames = ['cats', 'dogs'] 14 | 15 | test('app works', function (t) { 16 | const app = createApp(serviceNames) 17 | const {cats, dogs} = createModule(serviceNames) 18 | const catActions = cats.actions 19 | 20 | const updaters = reduxFp.combine({cats: cats.updater, dogs: dogs.updater}) 21 | const epics = combineEpics(cats.epic, dogs.epic) 22 | 23 | const reducer = (state, action) => updaters(action)(state) 24 | 25 | const epicMiddleware = createEpicMiddleware(epics, {dependencies: {feathers: app}}) 26 | 27 | const store = createStore(reducer, applyMiddleware(epicMiddleware)) 28 | 29 | const cidCreate = Cid() 30 | const cidUpdate = Cid() 31 | const cidPatch = Cid() 32 | const cidRemove = Cid() 33 | 34 | Store$(store) 35 | .filter((store) => store.cats && store.cats.cats[0]) 36 | .take(1) 37 | .mergeMap(({cats}) => { 38 | t.equal(cats.cats[0].name, 'fluffy') 39 | store.dispatch(catActions.update(cidUpdate, 0, {name: 'tick'})) 40 | return Store$(store) 41 | }) 42 | .filter((store) => store.cats && store.cats.cats[0] && store.cats.cats[0].name === 'tick') 43 | .take(1) 44 | .mergeMap(({cats}) => { 45 | t.equal(cats.cats[0].name, 'tick') 46 | store.dispatch(catActions.patch(cidPatch, 0, {nickName: 'fatboy'})) 47 | return Store$(store) 48 | }) 49 | .filter((store) => store.cats && store.cats.cats[0] && store.cats.cats[0].nickName === 'fatboy') 50 | .take(1) 51 | .mergeMap(({cats}) => { 52 | t.equal(cats.cats[0].nickName, 'fatboy') 53 | store.dispatch(catActions.remove(cidRemove, 0)) 54 | return Store$(store) 55 | }) 56 | .filter((store) => store.cats && !store.cats.cats[0]) 57 | .take(1) 58 | .subscribe(() => { 59 | t.pass() 60 | t.end() 61 | }) 62 | 63 | store.dispatch(cats.actions.create(cidCreate, {name: 'fluffy'})) 64 | }) 65 | 66 | function Store$ (store) { 67 | return rxjs.Observable.create(observer => { 68 | store.subscribe(() => { 69 | observer.next(store.getState()) 70 | }) 71 | }) 72 | } 73 | 74 | function createApp (resources) { 75 | const app = feathers() 76 | app.configure(feathersReactive(rxjs)) 77 | resources.forEach(resource => { 78 | app.use('/' + resource, memory()) 79 | }) 80 | return app 81 | } 82 | -------------------------------------------------------------------------------- /test/epic.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const Rx = require('rxjs/Rx') 3 | const Action$ = require('redux-observable/lib/cjs/ActionsObservable').ActionsObservable 4 | const { values, merge, mergeAll, propEq } = require('ramda') 5 | 6 | const catsData = { 7 | 0: { id: 0, name: 'honey', description: 'sweet and delicious.' }, 8 | 1: { id: 1, name: 'tea' }, 9 | 2: { id: 2, name: 'mug' } 10 | } 11 | const newCat = { name: 'honey', description: 'sweet and delicious' } 12 | const nextCat = { name: 'sugar' } 13 | 14 | const createActionTypes = require('../action-types') 15 | const createActionCreators = require('../actions') 16 | const createModule = require('../') 17 | 18 | const service = 'cats' 19 | const cats = createModule({ service }) 20 | const actionCreators = createActionCreators({ service }) 21 | 22 | var currentCid = 100 23 | function createCid () { 24 | return currentCid++ 25 | } 26 | 27 | test('find', function (t) { 28 | const cid = createCid() 29 | const action$ = Action$.of(cats.actions.find(cid)) 30 | const feathers = { 31 | find: () => Rx.Observable.of(values(catsData)) 32 | } 33 | const expected = [ 34 | actionCreators.start(cid, { service, method: 'find', args: { params: {} } }), 35 | actionCreators.set(cid, 0, catsData[0]), 36 | actionCreators.set(cid, 1, catsData[1]), 37 | actionCreators.set(cid, 2, catsData[2]), 38 | actionCreators.complete(cid) 39 | ] 40 | cats.epic(action$, undefined, { feathers }) 41 | .toArray() // .reduce(flip(append), []) 42 | .subscribe((actions) => { 43 | t.deepEqual(actions, expected) 44 | t.end() 45 | }) 46 | }) 47 | 48 | test('find with cancel', function (t) { 49 | const cid = createCid() 50 | const action$ = new Action$(Rx.Observable.create(observer => { 51 | observer.next(cats.actions.find(cid)) 52 | process.nextTick(() => { 53 | observer.next(cats.actions.complete(cid)) 54 | }) 55 | })) 56 | const feathers = { 57 | find: () => Rx.Observable.create(observer => { 58 | observer.next(values(catsData)) 59 | }) 60 | } 61 | const expected = [ 62 | actionCreators.start(cid, { service, method: 'find', args: { params: {} } }), 63 | actionCreators.set(cid, 0, catsData[0]), 64 | actionCreators.set(cid, 1, catsData[1]), 65 | actionCreators.set(cid, 2, catsData[2]) 66 | ] 67 | var i = 0 68 | cats.epic(action$, undefined, { feathers }) 69 | .subscribe((action) => { 70 | t.deepEqual(action, expected[i++]) 71 | if (i === 4) t.end() 72 | }) 73 | }) 74 | 75 | test('get', function (t) { 76 | const cid = createCid() 77 | const action$ = Action$.of(cats.actions.get(cid, 0)) 78 | const feathers = { 79 | get: (id) => Rx.Observable.of(catsData[id]) 80 | } 81 | const expected = [ 82 | actionCreators.start(cid, { service, method: 'get', args: { id: 0, params: {} } }), 83 | actionCreators.set(cid, 0, catsData[0]), 84 | actionCreators.complete(cid) 85 | ] 86 | cats.epic(action$, undefined, { feathers }) 87 | .toArray() 88 | .subscribe((actions) => { 89 | t.deepEqual(actions, expected) 90 | t.end() 91 | }) 92 | }) 93 | 94 | test('create', function (t) { 95 | const cid = createCid() 96 | const action$ = Action$.of(cats.actions.create(cid, newCat)) 97 | const feathers = { 98 | create: () => Rx.Observable.of(catsData[0]) 99 | } 100 | const expected = [ 101 | actionCreators.start(cid, { service, method: 'create', args: { data: newCat, params: {} } }), 102 | actionCreators.set(cid, cid, newCat), 103 | actionCreators.set(cid, cid, undefined), 104 | actionCreators.set(cid, 0, catsData[0]), 105 | actionCreators.complete(cid) 106 | ] 107 | cats.epic(action$, undefined, { feathers }) 108 | .toArray() 109 | .subscribe((actions) => { 110 | t.deepEqual(actions, expected) 111 | t.end() 112 | }) 113 | }) 114 | 115 | test('create with rollback', function (t) { 116 | const cid = createCid() 117 | const err = new Error('oh no') 118 | const action$ = Action$.of(cats.actions.create(cid, newCat)) 119 | const feathers = { 120 | create: () => Rx.Observable.throw(err) 121 | } 122 | const expected = [ 123 | actionCreators.start(cid, { service, method: 'create', args: { data: newCat, params: {} } }), 124 | actionCreators.set(cid, cid, newCat), 125 | actionCreators.set(cid, cid, undefined), 126 | actionCreators.error(cid, err) 127 | ] 128 | cats.epic(action$, undefined, { feathers }) 129 | .toArray() 130 | .subscribe((actions) => { 131 | t.deepEqual(actions, expected) 132 | t.end() 133 | }) 134 | }) 135 | 136 | test('update', function (t) { 137 | const cid = createCid() 138 | const action$ = Action$.of(cats.actions.update(cid, 0, nextCat)) 139 | const feathers = { 140 | update: () => Rx.Observable.of(merge({ id: 0, feathers: true }, nextCat)) 141 | } 142 | const store = { 143 | getState: () => ({ cats: catsData }) 144 | } 145 | const expected = [ 146 | actionCreators.start(cid, { service, method: 'update', args: { id: 0, data: nextCat, params: {} } }), 147 | actionCreators.set(cid, 0, merge({ id: 0 }, nextCat)), 148 | actionCreators.set(cid, 0, merge({ id: 0, feathers: true }, nextCat)), 149 | actionCreators.complete(cid) 150 | ] 151 | cats.epic(action$, store, { feathers }) 152 | .toArray() 153 | .subscribe((actions) => { 154 | t.deepEqual(actions, expected) 155 | t.end() 156 | }) 157 | }) 158 | 159 | test('update with rollback', function (t) { 160 | const cid = createCid() 161 | const err = new Error('oh no') 162 | const action$ = Action$.of(cats.actions.update(cid, 0, nextCat)) 163 | const feathers = { 164 | update: () => Rx.Observable.throw(err) 165 | } 166 | const store = { 167 | getState: () => ({ cats: catsData }) 168 | } 169 | const expected = [ 170 | actionCreators.start(cid, { service, method: 'update', args: { id: 0, data: nextCat, params: {} } }), 171 | actionCreators.set(cid, 0, merge({ id: 0 }, nextCat)), 172 | actionCreators.set(cid, 0, catsData[0]), 173 | actionCreators.error(cid, err) 174 | ] 175 | cats.epic(action$, store, { feathers }) 176 | .toArray() 177 | .subscribe((actions) => { 178 | t.deepEqual(actions, expected) 179 | t.end() 180 | }) 181 | }) 182 | 183 | test('patch', function (t) { 184 | const cid = createCid() 185 | const action$ = Action$.of(cats.actions.patch(cid, 0, nextCat)) 186 | const feathers = { 187 | patch: () => Rx.Observable.of(mergeAll([catsData[0], { feathers: true }, nextCat])) 188 | } 189 | const store = { 190 | getState: () => ({ cats: catsData }) 191 | } 192 | const expected = [ 193 | actionCreators.start(cid, { service, method: 'patch', args: { id: 0, data: nextCat, params: {} } }), 194 | actionCreators.set(cid, 0, merge(catsData[0], nextCat)), 195 | actionCreators.set(cid, 0, mergeAll([catsData[0], { feathers: true }, nextCat])), 196 | actionCreators.complete(cid) 197 | ] 198 | cats.epic(action$, store, { feathers }) 199 | .toArray() 200 | .subscribe((actions) => { 201 | t.deepEqual(actions, expected) 202 | t.end() 203 | }) 204 | }) 205 | 206 | test('patch with rollback', function (t) { 207 | const cid = createCid() 208 | const err = new Error('oh no') 209 | const action$ = Action$.of(cats.actions.patch(cid, 0, nextCat)) 210 | const feathers = { 211 | patch: () => Rx.Observable.throw(err) 212 | } 213 | const store = { 214 | getState: () => ({ cats: catsData }) 215 | } 216 | const expected = [ 217 | actionCreators.start(cid, { service, method: 'patch', args: { id: 0, data: nextCat, params: {} } }), 218 | actionCreators.set(cid, 0, merge(catsData[0], nextCat)), 219 | actionCreators.set(cid, 0, catsData[0]), 220 | actionCreators.error(cid, err) 221 | ] 222 | cats.epic(action$, store, { feathers }) 223 | .toArray() 224 | .subscribe((actions) => { 225 | t.deepEqual(actions, expected) 226 | t.end() 227 | }) 228 | }) 229 | 230 | test('remove', function (t) { 231 | const cid = createCid() 232 | const action$ = Action$.of(cats.actions.remove(cid, 0)) 233 | const feathers = { 234 | remove: () => Rx.Observable.of(catsData[0]) 235 | } 236 | const store = { 237 | getState: () => ({ cats: catsData }) 238 | } 239 | const expected = [ 240 | actionCreators.start(cid, { service, method: 'remove', args: { id: 0, params: {} } }), 241 | actionCreators.set(cid, 0, undefined), 242 | actionCreators.set(cid, 0, undefined), 243 | actionCreators.complete(cid) 244 | ] 245 | cats.epic(action$, store, { feathers }) 246 | .toArray() 247 | .subscribe((actions) => { 248 | t.deepEqual(actions, expected) 249 | t.end() 250 | }) 251 | }) 252 | 253 | test('remove with rollback', function (t) { 254 | const cid = createCid() 255 | const err = new Error('oh no') 256 | const action$ = Action$.of(cats.actions.remove(cid, 0)) 257 | const feathers = { 258 | remove: () => Rx.Observable.throw(err) 259 | } 260 | const store = { 261 | getState: () => ({ cats: catsData }) 262 | } 263 | const expected = [ 264 | actionCreators.start(cid, { service, method: 'remove', args: { id: 0, params: {} } }), 265 | actionCreators.set(cid, 0, undefined), 266 | actionCreators.set(cid, 0, catsData[0]), 267 | actionCreators.error(cid, err) 268 | ] 269 | cats.epic(action$, store, { feathers }) 270 | .toArray() 271 | .subscribe((actions) => { 272 | t.deepEqual(actions, expected) 273 | t.end() 274 | }) 275 | }) 276 | 277 | /* 278 | 279 | test('create is handled by epic and calls create on the feathers', function (t) { 280 | const cat = { name: 'fluffy' } 281 | const action$ = Action$.of(cats.actions.create(cat)) 282 | 283 | const feathers = { 284 | create: (data, params) => { 285 | t.deepEqual(data, cat) 286 | t.deepEqual(params, {}) 287 | t.end() 288 | } 289 | } 290 | 291 | cats.epic(action$, {}, { feathers }) 292 | .filter(isType(actionTypes.create)) 293 | .subscribe((action) => { 294 | }) 295 | }) 296 | 297 | test('create is handled by epic and emits request start action', function (t) { 298 | const cat = { name: 'fluffy' } 299 | const action$ = Action$.of(cats.actions.create(cat)) 300 | 301 | const feathers = { 302 | create: () => { 303 | } 304 | } 305 | 306 | cats.epic(action$, {}, { feathers }) 307 | .filter(isType(actionTypes.start)) 308 | .subscribe((action) => { 309 | t.ok(action) 310 | t.end() 311 | }) 312 | }) 313 | 314 | test('create is handled by epic and emits set action', function (t) { 315 | const cat = { name: 'fluffy' } 316 | const action$ = Action$.of(cats.actions.create(cat)) 317 | 318 | const feathers = { 319 | create: () => { 320 | } 321 | } 322 | 323 | cats.epic(action$, {}, { feathers }) 324 | .filter(({type}) => type === 'FEATHERS_CATS_SET') 325 | .subscribe((action) => { 326 | t.ok(action) 327 | t.end() 328 | }) 329 | }) 330 | 331 | test('create is handled by epic and emits set action twice when request succeeds', function (t) { 332 | const cat = { name: 'fluffy' } 333 | const action$ = Action$.of(cats.actions.create(cat)) 334 | t.plan(2) 335 | 336 | const feathers = { 337 | create: (cat) => { 338 | return Promise.resolve(cat) 339 | } 340 | } 341 | cats.epic(action$, {}, { feathers }) 342 | .filter(({type}) => type === 'FEATHERS_CATS_SET') 343 | .subscribe((action) => { 344 | t.ok(action) 345 | }) 346 | }) 347 | 348 | */ 349 | 350 | const isType = propEq('type') 351 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | const createModule = require('../') 5 | 6 | const moduleKeyNames = ['name', 'actions', 'updater', 'epic'] 7 | 8 | test('feathersAction is a function', function (t) { 9 | t.equal(typeof createModule, 'function') 10 | t.end() 11 | }) 12 | 13 | test('feathersAction.createModule called with a string returns object with action, updater and epic keys', function (t) { 14 | const module = createModule('cats') 15 | t.deepEqual(Object.keys(module), moduleKeyNames) 16 | t.end() 17 | }) 18 | 19 | test('feathersAction called with a object returns object with action, updater and epic keys', function (t) { 20 | const module = createModule({service: 'cats'}) 21 | t.deepEqual(Object.keys(module), moduleKeyNames) 22 | t.end() 23 | }) 24 | 25 | test('feathersAction called with an array of strings returns object with keys that match the strings', function (t) { 26 | const modules = createModule(['cats', 'dogs']) 27 | t.deepEqual(Object.keys(modules.cats), moduleKeyNames) 28 | t.deepEqual(Object.keys(modules.dogs), moduleKeyNames) 29 | t.end() 30 | }) 31 | 32 | test('feathersAction called with an array of objects returns object with keys that match the object names', function (t) { 33 | const modules = createModule([{service: 'cats'}, {service: 'dogs'}]) 34 | t.deepEqual(Object.keys(modules.cats), moduleKeyNames) 35 | t.deepEqual(Object.keys(modules.dogs), moduleKeyNames) 36 | t.end() 37 | }) 38 | 39 | test('throws if called with no args', function (t) { 40 | t.throws(() => createModule()) 41 | t.end() 42 | }) 43 | -------------------------------------------------------------------------------- /test/updater.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | const deepFreeze = require('deep-freeze') 5 | const assocPath = require('ramda/src/assocPath') 6 | const merge = require('ramda/src/merge') 7 | 8 | const createModule = require('../') 9 | const cats = createModule('cats') 10 | 11 | const catsRecords = { 12 | 0: { id: 0, name: 'honey', description: 'sweet and delicious.' }, 13 | 1: { id: 1, name: 'tea' }, 14 | 2: { id: 2, name: 'mug' } 15 | } 16 | 17 | const cat = catsRecords[0] 18 | 19 | const defaultServiceState = { cats: {} } 20 | const defaultRequestState = { feathers: {} } 21 | const defaultState = merge(defaultServiceState, defaultRequestState) 22 | 23 | deepFreeze(defaultState) 24 | deepFreeze(catsRecords) 25 | 26 | test('updater returns correct default state', function (t) { 27 | const state = cats.updater({type: 'woof'})() 28 | t.deepEqual(state, defaultState) 29 | t.end() 30 | }) 31 | 32 | test('set sets the new state by id', function (t) { 33 | const cid = 'abcd' 34 | const { actions, updater } = cats 35 | const expectedState = assocPath(['cats', cat.id], cat, defaultState) 36 | const action = actions.set(cid, cat.id, cat) 37 | 38 | const newState = updater(action)(defaultServiceState) 39 | t.deepEqual(newState, expectedState) 40 | t.end() 41 | }) 42 | 43 | test('start sets the request at the cid', function (t) { 44 | const { actions, updater } = cats 45 | const cid = 'abcd' 46 | const call = { 47 | method: 'create', 48 | service: 'cats', 49 | args: {} 50 | } 51 | const expectedState = assocPath(['feathers', cid], call, defaultState) 52 | 53 | const action = actions.start(cid, call) 54 | 55 | const newState = updater(action)(defaultState) 56 | t.deepEqual(newState, expectedState) 57 | t.end() 58 | }) 59 | 60 | test('success sets the result at the cid', function (t) { 61 | const { actions, updater } = cats 62 | const cid = 'abcd' 63 | const call = { 64 | method: 'create', 65 | service: 'cats', 66 | args: {} 67 | } 68 | const initialState = assocPath(['feathers', cid], call, defaultState) 69 | deepFreeze(initialState) 70 | 71 | const requestState = merge(call, { result: cat, error: null }) 72 | const expectedState = assocPath(['feathers', cid], requestState, initialState) 73 | 74 | const action = actions.complete(cid, cat) 75 | 76 | const newState = updater(action)(initialState) 77 | t.deepEqual(newState, expectedState) 78 | t.end() 79 | }) 80 | 81 | test('error sets the error at the cid', function (t) { 82 | const { actions, updater } = cats 83 | const cid = 'abcd' 84 | const call = { 85 | method: 'create', 86 | service: 'cats', 87 | args: {} 88 | } 89 | const initialState = assocPath(['feathers', cid], call, defaultState) 90 | deepFreeze(initialState) 91 | 92 | const err = new Error('oh no') 93 | const requestState = merge(call, { result: null, error: err }) 94 | const expectedState = assocPath(['feathers', cid], requestState, initialState) 95 | 96 | const action = actions.error(cid, err) 97 | 98 | const newState = updater(action)(initialState) 99 | t.deepEqual(newState, expectedState) 100 | t.end() 101 | }) 102 | -------------------------------------------------------------------------------- /updater.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const assoc = require('ramda/src/assoc') 4 | const assocPath = require('ramda/src/assocPath') 5 | const dissoc = require('ramda/src/dissoc') 6 | const pipe = require('ramda/src/pipe') 7 | const reduce = require('ramda/src/reduce') 8 | const __ = require('ramda/src/__') 9 | const { withDefaultState, concat, updateStateAt, handleActions, decorate } = require('redux-fp') 10 | 11 | const createActionTypes = require('./action-types') 12 | 13 | module.exports = createUpdater 14 | 15 | function createUpdater (options) { 16 | const { service } = options 17 | 18 | const actionTypes = createActionTypes(options) 19 | 20 | return concat( 21 | createServiceUpdater(actionTypes, service), 22 | createRequestUpdater(actionTypes) 23 | ) 24 | } 25 | 26 | function createServiceUpdater (actionTypes, service) { 27 | const serviceUpdateHandlers = { 28 | [actionTypes.set]: (action) => { 29 | const { id, data } = action.payload 30 | return assoc(id, data) 31 | }, 32 | [actionTypes.setAll]: (action) => { 33 | return reduce((sofar, next) => { 34 | return assoc(next.id, next, sofar) 35 | }, __, action.payload) 36 | }, 37 | [actionTypes.unset]: (action) => { 38 | const { id } = action.payload 39 | return dissoc(id) 40 | }, 41 | [actionTypes.unsetAll]: (action) => { 42 | return reduce((sofar, next) => { 43 | return dissoc(next.id, sofar) 44 | }, __, action.payload) 45 | } 46 | } 47 | 48 | return decorate( 49 | withDefaultState({}), 50 | updateStateAt(service), 51 | withDefaultState({}), 52 | handleActions(serviceUpdateHandlers) 53 | ) 54 | } 55 | 56 | function createRequestUpdater (actionTypes) { 57 | const requestUpdateHandlers = { 58 | [actionTypes.start]: action => { 59 | const { cid } = action.meta 60 | return pipe( 61 | assoc(cid, action.payload), 62 | assocPath([cid, 'isReady'], false) 63 | ) 64 | }, 65 | [actionTypes.ready]: action => { 66 | const { cid } = action.meta 67 | return assocPath([cid, 'isReady'], true) 68 | }, 69 | [actionTypes.complete]: action => { 70 | const { cid } = action.meta 71 | const result = action.payload 72 | const error = null 73 | return handleComplete({ cid, result, error }) 74 | }, 75 | [actionTypes.error]: action => { 76 | const { cid } = action.meta 77 | const result = null 78 | const error = action.payload 79 | return handleComplete({ cid, result, error }) 80 | } 81 | } 82 | 83 | function handleComplete ({ cid, result, error }) { 84 | return pipe( 85 | assocPath([cid, 'result'], result), 86 | assocPath([cid, 'error'], error) 87 | ) 88 | } 89 | 90 | return decorate( 91 | withDefaultState({}), 92 | updateStateAt('feathers'), 93 | withDefaultState({}), 94 | handleActions(requestUpdateHandlers) 95 | ) 96 | } 97 | --------------------------------------------------------------------------------