├── .editorconfig
├── .gitignore
├── .vscode
└── settings.json
├── CHANGELOG.md
├── LICENSE
├── docs
├── actions.md
├── async_action_creators.js
├── reducers.md
├── redux.md
└── tips.md
├── example
├── .gitignore
├── package.json
├── public
│ ├── favicon.ico
│ └── index.html
├── readme.md
├── src
│ ├── App.js
│ ├── index.js
│ ├── mocks.js
│ └── todos
│ │ ├── Form.js
│ │ ├── Index.js
│ │ ├── List.js
│ │ ├── New.js
│ │ ├── actions.js
│ │ ├── fixture.js
│ │ └── reducer.js
└── yarn.lock
├── package.json
├── readme.md
├── src
├── actionCreatorsFor.test.ts
├── actionCreatorsFor.ts
├── actionTypesFor.test.ts
├── actionTypesFor.ts
├── constants.ts
├── getDefaultConfig.ts
├── index.test.ts
├── index.ts
├── reducers
│ ├── common
│ │ ├── create
│ │ │ └── start.ts
│ │ ├── delete
│ │ │ └── start.ts
│ │ ├── reducersFor.ts
│ │ └── update
│ │ │ ├── error.ts
│ │ │ └── start.ts
│ ├── invariants.ts
│ ├── invariants
│ │ └── assertHasKey.ts
│ ├── list.ts
│ ├── list
│ │ ├── create
│ │ │ ├── error.test.ts
│ │ │ ├── error.ts
│ │ │ ├── start.test.ts
│ │ │ ├── start.ts
│ │ │ ├── success.test.ts
│ │ │ └── success.ts
│ │ ├── delete
│ │ │ ├── error.test.ts
│ │ │ ├── error.ts
│ │ │ ├── start.test.ts
│ │ │ ├── start.ts
│ │ │ ├── success.test.ts
│ │ │ └── success.ts
│ │ ├── fetch
│ │ │ ├── success.test.ts
│ │ │ └── success.ts
│ │ ├── invariants.ts
│ │ ├── reducersFor.test.ts
│ │ ├── reducersFor.ts
│ │ ├── store.ts
│ │ ├── store
│ │ │ ├── assert.ts
│ │ │ ├── merge.ts
│ │ │ └── remove.ts
│ │ └── update
│ │ │ ├── error.test.ts
│ │ │ ├── error.ts
│ │ │ ├── start.test.ts
│ │ │ ├── start.ts
│ │ │ ├── success.test.ts
│ │ │ └── success.ts
│ ├── map.ts
│ └── map
│ │ ├── create
│ │ ├── error.test.ts
│ │ ├── error.ts
│ │ ├── start.test.ts
│ │ ├── start.ts
│ │ ├── success.test.ts
│ │ └── success.ts
│ │ ├── delete
│ │ ├── error.test.ts
│ │ ├── error.ts
│ │ ├── start.test.ts
│ │ ├── start.ts
│ │ ├── success.test.ts
│ │ └── success.ts
│ │ ├── fetch
│ │ ├── success.test.ts
│ │ └── success.ts
│ │ ├── invariants.ts
│ │ ├── reducersFor.test.ts
│ │ ├── reducersFor.ts
│ │ ├── store.ts
│ │ ├── store
│ │ ├── assert.ts
│ │ ├── merge.ts
│ │ └── remove.ts
│ │ └── update
│ │ ├── error.test.ts
│ │ ├── error.ts
│ │ ├── start.test.ts
│ │ ├── start.ts
│ │ ├── success.test.ts
│ │ └── success.ts
├── types.ts
└── utils
│ ├── assertAllHaveKeys.ts
│ ├── assertNotArray.ts
│ ├── findByKey.ts
│ ├── makeScope.ts
│ └── wrapArray.ts
├── tsconfig.json
├── tslint.json
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # top-most EditorConfig file
2 | root = true
3 |
4 | # Unix-style newlines with a newline ending every file
5 | [*]
6 | end_of_line = lf
7 | insert_final_newline = true
8 | indent_style = space
9 | indent_size = 2
10 |
11 | [*.ts]
12 | indent_style = tab
13 | indent_size = 2
14 |
15 | [*.js]
16 | indent_style = tab
17 | indent_size = 4
18 |
19 | [*.json]
20 | indent_style = tab
21 | indent_size = 4
22 |
23 | [Makefile]
24 | indent_style = tab
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | npm-debug.log
4 | .idea
5 | dist
6 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "./node_modules/typescript/lib"
3 | }
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 3.2.0
4 |
5 | - Removed `_cid` after successful creation
6 |
7 | ## 3.1
8 |
9 | - Added `replace` option to `fetchSucess` action.
10 |
11 | ## 3.0.3
12 |
13 | - Fixed the List reducer adding a null client generated key when not needed.
14 |
15 | ## 3.0.2
16 |
17 | - Fix broken `Map.fetch.success reducer`
18 |
19 | ## 3.0
20 |
21 | - Added Map Store
22 | - Switch from lodash to ramda https://github.com/Versent/redux-crud/issues/39
23 |
24 | ## 2.0
25 |
26 | - Remove Seamless-Immutable and Immutable.js stores. These stores are not really necessary, as operations can be done with plain lodash without mutating the original collections. These libs were also adding a huge amount of weight to the library.
27 |
28 | You can wrap the collections with Seamless or Immutable.js after getting them from the store.
29 |
30 | - Remove dependency on the whole Lodash lib. This library now uses individual lodash functions as needed e.g. `lodash.assign`.
31 |
32 | ## **1.0**
33 |
34 | Added Immutable.js store
35 |
36 | **0.10.1** upgrade `action-names` dep, remove left over ES6
37 |
38 | **0.10.0** `.reducersFor` does not mutate the config object
39 |
40 | **0.9.0** Added mutable store (config.store: reduxCrud.STORE_MUTABLE)
41 |
42 | **0.8.0** Add `data` attribute to actions payload.
43 |
44 | **0.7.0** Replaced `unsaved` in createStart and updateStart with `pendingCreate` and `pendingUpdate`.
45 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016-present Versent
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/docs/actions.md:
--------------------------------------------------------------------------------
1 | # Actions
2 |
3 | ## `.actionTypesFor`
4 |
5 | Creates an object with standard CRUD action types:
6 | ```js
7 | var reduxCrud = require('redux-crud');
8 | var actionTypes = reduxCrud.actionTypesFor('users');
9 |
10 | // actionTypes =>
11 |
12 | {
13 | USERS_FETCH_START: 'USERS_FETCH_START',
14 | USERS_FETCH_SUCCESS: 'USERS_FETCH_SUCCESS',
15 | USERS_FETCH_ERROR: 'USERS_FETCH_ERROR',
16 |
17 | USERS_UPDATE_START: 'USERS_UPDATE_START',
18 | USERS_UPDATE_SUCCESS: 'USERS_UPDATE_SUCCESS',
19 | USERS_UPDATE_ERROR: 'USERS_UPDATE_ERROR',
20 |
21 | USERS_CREATE_START: 'USERS_CREATE_START',
22 | USERS_CREATE_SUCCESS: 'USERS_CREATE_SUCCESS',
23 | USERS_CREATE_ERROR: 'USERS_CREATE_ERROR',
24 |
25 | USERS_DELETE_START: 'USERS_DELETE_START',
26 | USERS_DELETE_SUCCESS: 'USERS_DELETE_SUCCESS',
27 | USERS_DELETE_ERROR: 'USERS_DELETE_ERROR',
28 |
29 | // Object also contains shortcuts
30 |
31 | fetchStart: 'USERS_FETCH_START',
32 | fetchSuccess: 'USERS_FETCH_SUCCESS',
33 | fetchError: 'USERS_FETCH_ERROR',
34 |
35 | updateStart: 'USERS_UPDATE_START',
36 | updateSuccess: 'USERS_UPDATE_SUCCESS',
37 | updateError: 'USERS_UPDATE_ERROR',
38 |
39 | createStart: 'USERS_CREATE_START',
40 | createSuccess: 'USERS_CREATE_SUCCESS',
41 | createError: 'USERS_CREATE_ERROR',
42 |
43 | deleteStart: 'USERS_DELETE_START',
44 | deleteSuccess: 'USERS_DELETE_SUCCESS',
45 | deleteError: 'USERS_DELETE_ERROR',
46 | }
47 | ```
48 |
49 | ## `.actionCreatorsFor`
50 |
51 | Generates the following action creators:
52 | - `fetchStart`
53 | - `fetchSuccess`
54 | - `fetchError`
55 | - `createStart`
56 | - `createSuccess`
57 | - `createError`
58 | - `updateStart`
59 | - `updateSuccess`
60 | - `updateError`
61 | - `deleteStart`
62 | - `deleteSuccess`
63 | - `deleteError`
64 |
65 | ```js
66 | var reduxCrud = require('redux-crud');
67 | var actionCreators = reduxCrud.actionCreatorsFor('users');
68 |
69 | // actionCreators =>
70 |
71 | {
72 | fetchStart: function(data) {
73 | return {
74 | data: data,
75 | type: 'USERS_FETCH_START',
76 | };
77 | },
78 |
79 | /*
80 | If data.replace is true, existing records in the store will
81 | be replaced instead of merged.
82 | */
83 | fetchSuccess: function(users, data) {
84 | return {
85 | data: data,
86 | records: users,
87 | type: 'USERS_FETCH_SUCCESS',
88 | };
89 | },
90 |
91 | fetchError: function(error, data) {
92 | return {
93 | data: data,
94 | error: error,
95 | type: 'USERS_FETCH_ERROR',
96 | };
97 | },
98 |
99 | /*
100 | The user record must have a client generated key
101 | so it can be inserted in the collection optimistically.
102 | */
103 | createStart: function(user, data) {
104 | return {
105 | data: data,
106 | record: user,
107 | type: 'USERS_CREATE_START',
108 | };
109 | },
110 |
111 | createSuccess: function(user, data) {
112 | return {
113 | data: data,
114 | record: user,
115 | type: 'USERS_CREATE_SUCCESS',
116 | };
117 | },
118 |
119 | /*
120 | The user record must have the client generated key
121 | so it can be matched with the record inserted optimistically.
122 | */
123 | createError: function(error, user, data) {
124 | return {
125 | data: data,
126 | error: error,
127 | record: user,
128 | type: 'USERS_CREATE_ERROR',
129 | };
130 | },
131 |
132 | updateStart: function(user, data) {
133 | return {
134 | data: data,
135 | record: user,
136 | type: 'USERS_UPDATE_START',
137 | };
138 | },
139 |
140 | updateSuccess: function(user, data) {
141 | return {
142 | data: data,
143 | record: user,
144 | type: 'USERS_UPDATE_SUCCESS',
145 | };
146 | },
147 |
148 | updateError: function(error, user, data) {
149 | return {
150 | data: data,
151 | error: error,
152 | record: user,
153 | type: 'USERS_UPDATE_ERROR',
154 | };
155 | },
156 |
157 | deleteStart: function(user, data) {
158 | return {
159 | data: data,
160 | record: user,
161 | type: 'USERS_DELETE_START',
162 | };
163 | },
164 |
165 | deleteSuccess: function(user, data) {
166 | return {
167 | data: data,
168 | record: user,
169 | type: 'USERS_DELETE_SUCCESS',
170 | };
171 | },
172 |
173 | deleteError: function(error, user, data) {
174 | return {
175 | data: data,
176 | error: error,
177 | record: user,
178 | type: 'USERS_DELETE_ERROR',
179 | };
180 | }
181 | }
182 | ```
183 |
184 | `actionCreatorsFor` takes an optional config object as second argument:
185 |
186 | ```js
187 | reduxCrud.actionCreatorsFor('users', {key: '_id'});
188 | ```
189 |
190 | Don't forget to do the same thing in reducers, with `reducersFor`:
191 |
192 | ```js
193 | reduxCrud.Map.reducersFor('users', {key: '_id'});
194 | ```
195 |
196 | ### The `data` attribute
197 |
198 | The `data` attribute in the actions payload is optional. The reducer doesn't do anything with this. This is only provided in case you want to pass extra information in the actions.
199 |
--------------------------------------------------------------------------------
/docs/async_action_creators.js:
--------------------------------------------------------------------------------
1 | import reduxCrud from 'redux-crud';
2 | import _ from 'lodash';
3 | import cuid from 'cuid';
4 |
5 | const baseActionCreators = reduxCrud.actionCreatorsFor('users');
6 |
7 | let actionCreators = {
8 |
9 | fetchOne(id) {
10 | return function(dispatch) {
11 | const action = baseActionCreators.fetchStart();
12 | dispatch(action);
13 |
14 | // send the request
15 | const url = `/users/${id}`;
16 | const promise = someAjaxLibrary({
17 | url: url,
18 | method: 'GET'
19 | });
20 |
21 | promise.then(function(response) {
22 | const user = response.data.data;
23 | const action = baseActionCreators.fetchSuccess(user);
24 | dispatch(action);
25 | }, function(response) {
26 | // dispatch the error action
27 | // first param is the error
28 | const action = baseActionCreators.fetchError(response);
29 | dispatch(action);
30 | }).catch(function(err) {
31 | console.error(err.toString());
32 | });
33 |
34 | return promise;
35 | }
36 | },
37 |
38 | fetch(page, limit, replaceExisting) {
39 | return function(dispatch) {
40 | const action = baseActionCreators.fetchStart();
41 | dispatch(action);
42 |
43 | // send the request
44 | // e.g. /users?page=1&limit=20
45 | const url = `/users`;
46 | const promise = someAjaxLibrary({
47 | url: url,
48 | method: 'GET',
49 | data: {
50 | page: page,
51 | limit : limit
52 | }
53 | });
54 |
55 | promise.then(function(response) {
56 | const users = response.data.data;
57 | const action = baseActionCreators.fetchSuccess(users, {replace: replaceExisting});
58 | dispatch(action);
59 | }, function(response) {
60 | // dispatch the error action
61 | // first param is the error
62 | const action = baseActionCreators.fetchError(response);
63 | dispatch(action);
64 | }).catch(function(err) {
65 | console.error(err.toString());
66 | });
67 |
68 | return promise;
69 | }
70 | },
71 |
72 | create(user) {
73 | return function(dispatch) {
74 | // Generate a cid so we can match the records
75 | var cid = cuid();
76 | user = user.merge({id: cid});
77 |
78 | // optimistic creation
79 | const action = baseActionCreators.createStart(user);
80 | dispatch(action);
81 |
82 | // send the request
83 | const url = `/users/`;
84 | const promise = someAjaxLibrary({
85 | url: url,
86 | method: 'POST',
87 | data: {
88 | user
89 | }
90 | });
91 |
92 | promise.then(function(response) {
93 | const returnedUser = response.data.data;
94 | const action = baseActionCreators.createSuccess(returnedUser, cid);
95 | dispatch(action);
96 | }, function(response) {
97 | const action = baseActionCreators.createError(response, user);
98 | dispatch(action);
99 | }).catch(function(err) {
100 | console.error(err.toString());
101 | });
102 |
103 | return promise;
104 | }
105 | },
106 |
107 | update(user) {
108 | return function(dispatch) {
109 | // optimistic update
110 | const action = baseActionCreators.updateStart(user);
111 | dispatch(action);
112 |
113 | // send the request
114 | const url = `/users/${user.id}`;
115 | const promise = someAjaxLibrary({
116 | url: url,
117 | method: 'PATCH',
118 | data: {
119 | user
120 | }
121 | });
122 |
123 | promise.then(function(response) {
124 | const returnedUser = response.data.data;
125 | const action = baseActionCreators.updateSuccess(returnedUser);
126 | dispatch(action);
127 | }, function(response) {
128 | const action = baseActionCreators.updateError(response, user);
129 | dispatch(action);
130 | }).catch(function(err) {
131 | console.error(err.toString());
132 | });
133 |
134 | return promise;
135 | }
136 | },
137 |
138 | delete(user) {
139 | return function(dispatch) {
140 | // optimistic delete
141 | const action = baseActionCreators.deleteStart(user);
142 | dispatch(action);
143 |
144 | // send the request
145 | const url = `/users/${user.id}`;
146 | const promise = someAjaxLibrary({
147 | url: url,
148 | method: 'DELETE'
149 | });
150 |
151 | promise.then(function(response) {
152 | const returnedUser = response.data.data;
153 | const action = baseActionCreators.deleteSuccess(returnedUser);
154 | dispatch(action);
155 | }, function(response) {
156 | const action = baseActionCreators.deleteError(response, user);
157 | dispatch(action);
158 | }).catch(function(err) {
159 | console.error(err.toString());
160 | });
161 |
162 | return promise;
163 | }
164 | },
165 |
166 | }
167 |
168 | actionCreators = _.extend(baseActionCreators, actionCreators);
169 |
170 | export default actionCreators;
171 |
172 |
--------------------------------------------------------------------------------
/docs/reducers.md:
--------------------------------------------------------------------------------
1 | # Reducers
2 |
3 | ## `.List.reducersFor` and `.Map.reducersFor`
4 |
5 | There are `reducersFor` for each type of store:
6 |
7 | ```js
8 | var reduxCrud = require('redux-crud');
9 | var reducers = reduxCrud.List.reducersFor('users');
10 |
11 | // or
12 |
13 | var reducers = reduxCrud.Map.reducersFor('users');
14 | ```
15 |
16 | `reducersFor` creates a reducer function for the given resource. Redux CRUD assumes that all records will have a unique key, e.g. `id`.
17 |
18 | It generates the following reducers:
19 |
20 | - `fetchSuccess`
21 | - `createStart`
22 | - `createSuccess`
23 | - `createError`
24 | - `updateStart`
25 | - `updateSuccess`
26 | - `updateError`
27 | - `deleteStart`
28 | - `deleteSuccess`
29 | - `deleteError`
30 |
31 | *Note: There are no `fetchStart` and `fetchError` reducers.*
32 |
33 | ```js
34 | var reduxCrud = require('redux-crud');
35 | var reducers = reduxCrud.List.reducersFor('users');
36 |
37 | // reducers =>
38 |
39 | function (state, action) {
40 | switch (action.type) {
41 | case 'USERS_FETCH_SUCCESS':
42 | ...
43 | case 'USERS_CREATE_START':
44 | ...
45 | case 'USERS_CREATE_SUCCESS':
46 | ...
47 | }
48 | }
49 | ```
50 |
51 | `reducersFor` takes an optional config object as second argument:
52 |
53 | ```js
54 | reduxCrud.List.reducersFor('users', {key: '_id'});
55 | ```
56 |
57 | Don't forget to do the same thing in actions, with `actionCreatorsFor`:
58 |
59 | ```js
60 | reduxCrud.actionCreatorsFor('users', {key: '_id'});
61 | ```
62 |
63 | __config.key__
64 |
65 | Key to be used for merging records. Default: 'id'.
66 |
67 | ## What each reducer does
68 |
69 | ### `fetchSuccess`
70 |
71 | Listens for an action like this (generated by `actionCreatorsFor`):
72 |
73 | ```js
74 | {
75 | records: users,
76 | type: 'USERS_FETCH_SUCCESS',
77 | }
78 | ```
79 |
80 | Takes one record or an array of records and adds them to the current state. Uses the given `key` or `id` by default to merge.
81 | If `data.replace` is set to true and passed to the action, existing records will be replaced by the ones provided and not merged (default behaviour).
82 |
83 | ### `createStart`
84 |
85 | Listens for an action like:
86 |
87 | ```js
88 | {
89 | type: 'USERS_CREATE_START',
90 | record: user,
91 | }
92 | ```
93 |
94 | Adds the record optimistically to the collection. The record must have a client generated key e.g. `id`, otherwise the reducer will throw an error. This key is necessary for matching records on `createSuccess` and `createError`. This client generated key is just temporary, is not expected that you will use this key when saving your data in the backend, it is just there so records can be matched.
95 |
96 | __This action is optional, dispatch this only if you want optimistic creation.__ [Read more about this](#about-optimistic-changes).
97 |
98 | For generating keys see [cuid](https://github.com/ericelliott/cuid).
99 |
100 | Also adds `busy` and `pendingCreate` to the record so you can display proper indicators in your UI.
101 |
102 | ### `createSuccess`
103 |
104 | Listens for an action like this (generated by `actionCreatorsFor`):
105 |
106 | ```js
107 | {
108 | type: 'USERS_CREATE_SUCCESS',
109 | record: user,
110 | cid: clientGeneratedId
111 | }
112 | ```
113 |
114 | Takes one record and adds it to the current state. Uses the given `key` (`id` by default) to merge.
115 |
116 | The `cid` attribute is optional but it should be used when dispatching `createStart`. This `cid` will be used for matching the record and replacing it with the saved one.
117 |
118 | ### `createError`
119 |
120 | Listens for an action like:
121 |
122 | ```js
123 | {
124 | type: 'USERS_CREATE_ERROR',
125 | record: user,
126 | }
127 | ```
128 |
129 | This reducer removes the record from the collection. The record key is used for matching the records. So if a record was added optimistically using `createStart` then the keys must match.
130 |
131 | ### `updateStart`
132 |
133 | Listens for an action like this (generated by `actionCreatorsFor`):
134 |
135 | ```js
136 | {
137 | type: 'USERS_UPDATE_START',
138 | record: user
139 | }
140 | ```
141 |
142 | Takes one record and merges it to the current state. Uses the given `key` or `id` by default to merge.
143 |
144 | It also add these two properties to the record:
145 | - `busy`
146 | - `pendingUpdate`
147 |
148 | You can use this to display relevant information in the UI e.g. a spinner.
149 |
150 | ### `updateSuccess`
151 |
152 | Listens for an action like this (generated by `actionCreatorsFor`):
153 |
154 | ```js
155 | {
156 | type: 'USERS_UPDATE_SUCCESS',
157 | record: user
158 | }
159 | ```
160 |
161 | Takes one record and merges it to the current state. Uses the given `key` or `id` by default to merge.
162 |
163 | ### `updateError`
164 |
165 | Listens for an action like this (generated by `actionCreatorsFor`):
166 |
167 | ```js
168 | {
169 | type: 'USERS_UPDATE_ERROR',
170 | record: user,
171 | error: error
172 | }
173 | ```
174 |
175 | This reducer will remove `busy` from the given record. It will not rollback the record to their previous state as we don't want users to lose their changes. The record will keep the `pendingUpdate` attribute set to true.
176 |
177 | ## `deleteStart`
178 |
179 | Listens for an action like this (generated by `actionCreatorsFor`):
180 |
181 | ```js
182 | {
183 | type: 'USERS_DELETE_START',
184 | record: user
185 | }
186 | ```
187 |
188 | Marks the given record as `deleted` and `busy`. This reducer doesn't actually remove it. In your UI you can filter out records with `deleted` to hide them.
189 |
190 | ## `deleteSuccess`
191 |
192 | Listens for an action like this (generated by `actionCreatorsFor`):
193 |
194 | ```js
195 | {
196 | type: 'USERS_DELETE_SUCCESS',
197 | record: user
198 | }
199 | ```
200 |
201 | This reducer removes the given record from the store.
202 |
203 | ## `deleteError`
204 |
205 | Listens for an action like this (generated by `actionCreatorsFor`):
206 |
207 | ```js
208 | {
209 | type: 'USERS_DELETE_ERROR',
210 | record: user,
211 | error: error
212 | }
213 | ```
214 |
215 | Removes `deleted` and `busy` from the given record.
216 |
--------------------------------------------------------------------------------
/docs/redux.md:
--------------------------------------------------------------------------------
1 | # Using with Redux
2 |
3 | ### Action creators
4 |
5 | Create your action creators by extending the standard actions:
6 | ```js
7 | import _ from 'lodash';
8 | import reduxCrud from 'redux-crud';
9 |
10 | const standardActionCreators = reduxCrud.actionCreatorsFor('users');
11 |
12 | let actionCreators = {
13 | update(user) {
14 | ...
15 | }
16 | }
17 |
18 | actionCreators = _.assign(standardActionCreators, actionCreators);
19 |
20 | export default actionCreators;
21 | ```
22 |
23 | ### Async action creators
24 |
25 | Redux CRUD only generates sync action creators. Async action creators still need to be added:
26 | ```js
27 |
28 | const standardActionCreators = reduxCrud.actionCreatorsFor('users');
29 |
30 | let actionCreators = {
31 | update(user) {
32 | return function(dispatch) {
33 | // dispatch a `updateStart` for optimistic updates
34 | const action = standardActionCreators.updateStart(user);
35 | dispatch(action);
36 |
37 | // send the request
38 | const url = `/users/${user.id}`;
39 | const promise = someAjaxLibrary({
40 | url: url,
41 | method: 'PUT',
42 | data: {
43 | user
44 | }
45 | });
46 |
47 | promise.then(function(response) {
48 | // dispatch the success action
49 | const returnedUser = response.data.data;
50 | const action = standardActionCreators.updateSuccess(returnedUser);
51 | dispatch(action);
52 | }, function(response) {
53 | // rejection
54 | // dispatch the error action
55 | // first param is the error
56 | const action = standardActionCreators.updateError(response, user);
57 | dispatch(action);
58 | }).catch(function(err) {
59 | console.error(err.toString());
60 | });
61 |
62 | return promise;
63 | }
64 | },
65 | ...
66 | }
67 | ```
68 |
69 | [See a list of examples here](./async_action_creators.js)
70 |
71 | ### Reducers
72 |
73 | Redux CRUD generates standard reducers for __`fetch`__, __`create`__, __`update`__ and __`delete`__.
74 |
75 | Create your Redux application:
76 | ```js
77 | import thunkMiddleware from 'redux-thunk';
78 | import loggerMiddleware from 'redux-logger';
79 | import { combineReducers } from 'redux';
80 | import { createStore, applyMiddleware } from 'redux';
81 | import reduxCrud from 'redux-crud';
82 |
83 | const createStoreWithMiddleware = applyMiddleware(
84 | thunkMiddleware, // lets us dispatch() functions
85 | loggerMiddleware // neat middleware that logs actions
86 | )(createStore);
87 |
88 | const allReducers = combineReducers({
89 | users: reduxCrud.Map.reducersFor('users'),
90 | posts: reduxCrud.Map.reducersFor('posts'),
91 | });
92 |
93 | const store = createStoreWithMiddleware(allReducers);
94 | ```
95 |
96 | ### Extending reducers
97 |
98 | There are many cases when the generated reducers are not enough. For example you might want to delete relevant `comments` when a `post` is deleted. You can extend a reducer function like this:
99 |
100 | ```js
101 | // comments/reducers.js
102 |
103 | import reduxCrud from 'redux-crud';
104 |
105 | const standardReducers = reduxCrud.Map.reducersFor('comments');
106 |
107 | function reducers(state=[], action) {
108 | switch(action.type) {
109 | case 'POSTS_DELETE_SUCCESS':
110 | // ...delete comments for the given post and return a new state for comments
111 | return state;
112 | default:
113 | // pass to the generated reducers
114 | return standardReducers(state, action);
115 | }
116 | }
117 |
118 | export default reducers;
119 | ```
120 |
121 | Then you can use this reducer:
122 |
123 | ```js
124 | import commentsReducers from './comments/reducers';
125 |
126 | const allReducers = combineReducers({
127 | comments: commentsReducers,
128 | posts: reduxCrud.Map.reducersFor('posts'),
129 | });
130 | ```
131 |
--------------------------------------------------------------------------------
/docs/tips.md:
--------------------------------------------------------------------------------
1 | # Tips
2 |
3 | ### Getting data to your components
4 |
5 | With React use [React-Redux](https://github.com/rackt/react-redux).
6 |
7 | ## Avoid nesting
8 |
9 | Don't atttempt to store nested resources. e.g. `{id: 1, posts: [{...}]}`. This makes harder to keep the information in sync with the UI. Instead always normalize the resources when they arrive from the server and store them in collections of their own.
10 |
11 | ### Normalizing records
12 |
13 | Your API might return something like:
14 |
15 | ```js
16 | {
17 | id: 1,
18 | label: 'Some post',
19 | comments: [
20 | {id: 1, body: '...'},
21 | {id: 2, body: '...'},
22 | ]
23 | }
24 | ```
25 |
26 | Instead of trying to work with nested records in your views, you should normalize them in your async action creator:
27 |
28 | ```js
29 |
30 | const baseActionCreators = reduxCrud.actionCreatorsFor('posts');
31 | const baseCommentsActionCreators = reduxCrud.actionCreatorsFor('comments');
32 |
33 | fetch() {
34 | return function(dispatch) {
35 | const action = baseActionCreators.fetchStart();
36 | dispatch(action);
37 |
38 | const url = `/posts/`;
39 | const promise = someAjaxLibrary({
40 | url: url,
41 | method: 'GET'
42 | });
43 |
44 | promise.then(function(response) {
45 | const posts = response.data.data;
46 | const action = baseActionCreators.fetchSuccess(posts);
47 | dispatch(action);
48 |
49 | /***********************************************/
50 | /* Get the comments and send them to the store */
51 | const comments = _(posts).map(function(post) {
52 | return post.comments;
53 | }).flatten().value();
54 |
55 | const commentsAction = baseCommentsActionCreators.fetchSuccess(comments);
56 | dispatch(commentsAction);
57 | /**********************************************/
58 | }, function(response) {
59 | const action = baseActionCreators.fetchError(response);
60 | dispatch(action);
61 | }).catch(function(err) {
62 | console.error(err.toString());
63 | });
64 |
65 | return promise;
66 | }
67 | },
68 | ```
69 |
70 | ### Use plural resources
71 |
72 | Use a collection of resources and name them using the plural form e.g. `users` instead of `user`.
73 |
74 | ### About optimistic changes
75 |
76 | Dispatching `createStart`, `updateStart` and `deleteStart` will result in optimistic changes to your store. See the description of what each reducer does above. `updateStart` and `deleteStart` will just work out of the box. `createStart` needs additional code from you.
77 |
78 | This is an example async action creator with optimistic creation:
79 |
80 | ```js
81 | create(user) {
82 | return function(dispatch) {
83 | // Generate a cid so we can match the records
84 | var cid = cuid();
85 |
86 | // Add the cid as the primary key
87 | user = user.merge({id: cid});
88 |
89 | // Optimistic creation
90 | // This action creator will throw if user doesn't have a primary key
91 | const action = baseActionCreators.createStart(user);
92 | dispatch(action);
93 |
94 | // send the request
95 | const url = `/users/`;
96 | const promise = someAjaxLibrary({
97 | url: url,
98 | method: 'POST',
99 | data: {
100 | user
101 | }
102 | });
103 |
104 | promise.then(function(response) {
105 | const returnedUser = response.data.data;
106 | // We need to pass the cid as the second argument
107 | const action = baseActionCreators.createSuccess(returnedUser, cid);
108 | dispatch(action);
109 | }, function(response) {
110 | const action = baseActionCreators.createError(response, user);
111 | dispatch(action);
112 | }).catch(function(err) {
113 | console.error(err.toString());
114 | });
115 |
116 | return promise;
117 | }
118 | },
119 | ```
120 |
121 | Note how we need to pass the `cid` as the second argument to `createSuccess`. __If we don't the reducer will not be able to match the records and you will end up with duplicates__.
122 |
123 | Adding a client generated `id` to a record doesn't mean that you need to use that `id` for saving it in the backend. You can still generate ids as usual in your DB.
124 |
125 | When the record comes back saved from the server the reducer will try to match `id` on the optimistically inserted record with `cid` on the `createSuccess` action. If it finds a match it will replace the optimistically inserted record with the given one. That record will now have the normal `id` given by the backend (The client generated id is thrown away at this point).
126 |
127 | ### Pending attributes
128 |
129 | `createStart` and `updateStart` will add the following attributes:
130 |
131 | - **createStart**: Adds `busy` and `pendingCreate`
132 | - **updateStart**: Adds `busy` and `pendingUpdate`
133 | - **updateError**: Removes `busy` but leaves `pendingUpdate`
134 |
135 | You can use these special attributes for showing indicators and preventing navigation:
136 |
137 | - Show a busy indicator when `busy` is true.
138 | - Do not allow navigation to a resource when `pendingCreate` is true.
139 | - Show a _retry_ button when an update fails: `busy` is false but `pendingUpdate` is true.
140 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 |
6 | # testing
7 | coverage
8 |
9 | # production
10 | build
11 |
12 | # misc
13 | .DS_Store
14 | .env
15 | npm-debug.log
16 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "devDependencies": {
6 | "ace-css": "^1.1.0",
7 | "axios": "^0.15.3",
8 | "axios-mock-adapter": "^1.7.1",
9 | "bows": "^1.6.0",
10 | "cuid": "^1.3.8",
11 | "invariant": "^2.2.2",
12 | "ramda": "^0.23.0",
13 | "react-fa": "^4.1.2",
14 | "react-redux": "^5.0.2",
15 | "react-scripts": "0.8.5",
16 | "redux": "^3.6.0",
17 | "redux-logger": "^2.7.4",
18 | "redux-thunk": "^2.2.0"
19 | },
20 | "dependencies": {
21 | "react": "^15.4.2",
22 | "react-dom": "^15.4.2"
23 | },
24 | "scripts": {
25 | "start": "react-scripts start",
26 | "build": "react-scripts build",
27 | "test": "react-scripts test --env=jsdom",
28 | "eject": "react-scripts eject"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/example/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Versent/redux-crud/5011cc55a7518823c77bf519b82b881f54ca4039/example/public/favicon.ico
--------------------------------------------------------------------------------
/example/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
16 | React App
17 |
18 |
19 |
20 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/example/src/App.js:
--------------------------------------------------------------------------------
1 | // import loggerMiddleware from 'redux-logger'
2 | import { Provider } from 'react-redux'
3 | import * as redux from "redux"
4 | import bows from "bows"
5 | import initMocks from "./mocks"
6 | import React from "react"
7 | import thunkMiddleware from 'redux-thunk'
8 |
9 | import todosReducer from './todos/reducer'
10 | import TodosIndex from './todos/Index'
11 |
12 | var log = bows("App")
13 |
14 | log("App")
15 |
16 | initMocks()
17 |
18 | const allReducers = redux.combineReducers({
19 | todos: todosReducer,
20 | })
21 |
22 | const store = redux.createStore(
23 | allReducers,
24 | redux.compose(
25 | redux.applyMiddleware(thunkMiddleware)
26 | )
27 | )
28 |
29 | class App extends React.Component {
30 | render() {
31 | return (
32 |
33 |
36 |
37 | )
38 | }
39 | }
40 |
41 | export default App
42 |
--------------------------------------------------------------------------------
/example/src/index.js:
--------------------------------------------------------------------------------
1 | import '../node_modules/ace-css/css/ace.css'
2 | import React from "react";
3 | import ReactDOM from "react-dom";
4 | import App from "./App";
5 |
6 | ReactDOM.render(
7 | ,
8 | document.getElementById("root")
9 | );
10 |
--------------------------------------------------------------------------------
/example/src/mocks.js:
--------------------------------------------------------------------------------
1 | import axios from "axios"
2 | import AxiosMock from "axios-mock-adapter"
3 | import r from "ramda"
4 | import fixture from "./todos/fixture"
5 |
6 | var mock = new AxiosMock(axios, { delayResponse: 500 })
7 | var nextMockId = 100
8 |
9 | export default function init() {
10 | mock.onGet("/todos").reply(200, fixture)
11 |
12 | mock.onPost("/todos").reply(function(config) {
13 | nextMockId++
14 | var record = JSON.parse(config.data)
15 | record = r.merge(record, {
16 | id: nextMockId
17 | })
18 | return [200, record]
19 | })
20 |
21 | mock.onPatch(/\/todos\/\d+/).reply(function(config) {
22 | return [200, config.data]
23 | })
24 |
25 | mock.onDelete(/\/todos\/\d+/).reply(200)
26 | }
27 |
--------------------------------------------------------------------------------
/example/src/todos/Form.js:
--------------------------------------------------------------------------------
1 | // import bows from "bows"
2 | import Icon from "react-fa"
3 | import r from "ramda"
4 | import React from "react"
5 |
6 | const PT = React.PropTypes
7 | // const log = bows("todos--Form")
8 |
9 | class Form extends React.Component {
10 |
11 | constructor(props, ctx) {
12 | super(props, ctx)
13 | this.state = this.getCleanState(props)
14 | }
15 |
16 | componentWillReceiveProps(nextProps) {
17 | this.setState(this.getCleanState(nextProps))
18 | }
19 |
20 | getCleanState(props) {
21 | const todo = props.todo
22 | return {
23 | title: todo.title,
24 | }
25 | }
26 |
27 | onChange(event) {
28 | const { value } = event.target
29 | this.setState({
30 | title: value,
31 | })
32 | }
33 |
34 | onSave(event) {
35 | event.preventDefault()
36 | var todo = r.merge(this.props.todo, this.state)
37 | this.props.onCommit(todo)
38 | }
39 |
40 | render() {
41 | return (
42 |
57 | )
58 | }
59 |
60 | }
61 |
62 | Form.propTypes = {
63 | dispatch: PT.func.isRequired,
64 | onCommit: PT.func.isRequired,
65 | todo: PT.object.isRequired,
66 | }
67 |
68 | export default Form
69 |
--------------------------------------------------------------------------------
/example/src/todos/Index.js:
--------------------------------------------------------------------------------
1 | import { connect } from "react-redux"
2 | import actions from "./actions"
3 | import invariant from "invariant"
4 | import List from "./List"
5 | import New from "./New"
6 | import React from "react"
7 |
8 | const PT = React.PropTypes
9 |
10 | class Index extends React.Component {
11 |
12 | componentDidMount() {
13 | this.fetchTodos()
14 | }
15 |
16 | get dispatch() {
17 | return this.props.dispatch
18 | }
19 |
20 | fetchTodos() {
21 | const action = actions.fetch()
22 | this.dispatch(action)
23 | }
24 |
25 | render() {
26 | var props = this.props
27 |
28 | invariant(props.dispatch, "Required dispatch")
29 | invariant(props.todos, "Required todos")
30 |
31 | return (
32 |
33 | Todos
34 |
35 |
39 |
40 | )
41 | }
42 | }
43 |
44 | Index.propTypes = {
45 | dispatch: PT.func.isRequired,
46 | }
47 |
48 | export default connect(state => state)(Index)
49 |
--------------------------------------------------------------------------------
/example/src/todos/List.js:
--------------------------------------------------------------------------------
1 | // import bows from "bows"
2 | import actions from "./actions"
3 | import Icon from "react-fa"
4 | import invariant from "invariant"
5 | import r from "ramda"
6 | import React from "react"
7 |
8 | var PT = React.PropTypes
9 | var baseClass = "todos-List"
10 | // var log = bows(baseClass)
11 |
12 | class List extends React.Component {
13 |
14 | onToggle(todo, done, event) {
15 | event.preventDefault()
16 | todo = r.merge(todo, {done})
17 | const action = actions.update(todo)
18 | this.props.dispatch(action)
19 | }
20 |
21 | onDelete(todo, event) {
22 | event.preventDefault()
23 | const action = actions.delete(todo)
24 | this.props.dispatch(action)
25 | }
26 |
27 | onRename() {
28 | const action = actions.renameAll()
29 | this.props.dispatch(action)
30 | }
31 |
32 | onShuffleName() {
33 | const action = actions.shuffleName()
34 | this.props.dispatch(action)
35 | }
36 |
37 | renderCheck(todo) {
38 | if (todo.done) {
39 | return (
40 |
44 |
45 |
46 | )
47 | } else {
48 | return (
49 |
53 |
54 |
55 | )
56 | }
57 | }
58 |
59 | renderTodos() {
60 | return this.props.todos.map(todo => {
61 | return (
62 |
63 |
64 | {todo.title}
65 | |
66 |
67 | {this.renderCheck(todo)}
68 |
71 | |
72 |
73 | )
74 | })
75 | }
76 |
77 | render() {
78 | var props = this.props
79 |
80 | invariant(props.todos, "Required todos")
81 | invariant(props.dispatch, "Required dispatch")
82 |
83 | return (
84 |
85 |
86 |
87 |
88 | Title |
89 | |
90 |
91 |
92 |
93 | {this.renderTodos()}
94 |
95 |
96 |
97 | )
98 | }
99 | }
100 |
101 | List.propTypes = {
102 | todos: PT.array.isRequired,
103 | dispatch: PT.func.isRequired,
104 | }
105 |
106 | export default List
107 |
--------------------------------------------------------------------------------
/example/src/todos/New.js:
--------------------------------------------------------------------------------
1 | import actions from "./actions"
2 | // import bows from "bows"
3 | import Form from "./Form"
4 | import invariant from "invariant"
5 | import React from "react"
6 |
7 | var PT = React.PropTypes
8 | // var log = bows("todos-New")
9 |
10 | class New extends React.Component {
11 |
12 | constructor(props, ctx) {
13 | super(props, ctx)
14 | this.state = this.getCleanState()
15 | }
16 |
17 | getCleanState() {
18 | return {
19 | todo: {
20 | title: "",
21 | },
22 | }
23 | }
24 |
25 | onCommit(todo) {
26 | const action = actions.create(todo)
27 | const dispatch = this.props.dispatch
28 | dispatch(action)
29 | this.setState(this.getCleanState())
30 | }
31 |
32 | render() {
33 | var { props, state } = this
34 |
35 | invariant(props.dispatch, "Required dispatch")
36 |
37 | return (
38 |
45 | )
46 | }
47 |
48 | }
49 |
50 | New.propTypes = {
51 | dispatch: PT.func.isRequired,
52 | }
53 |
54 | export default New
55 |
--------------------------------------------------------------------------------
/example/src/todos/actions.js:
--------------------------------------------------------------------------------
1 | import axios from "axios"
2 | import bows from "bows"
3 | import cuid from "cuid"
4 | import r from "ramda"
5 | import reduxCrud from "../../../dist/index"
6 |
7 | var baseActionCreators = reduxCrud.actionCreatorsFor("todos")
8 | var log = bows("todos-actions")
9 |
10 | let actionCreators = {
11 |
12 | fetch() {
13 | log("fetch todos")
14 | return function(dispatch, getState) {
15 |
16 | const action = baseActionCreators.fetchStart()
17 | log("action", action)
18 | dispatch(action)
19 |
20 | // send the request
21 | const url = "/todos"
22 | const promise = axios({
23 | url: url,
24 | })
25 |
26 | promise.then(function(response) {
27 | log("success", response)
28 | // dispatch the success action
29 | const returned = response.data
30 | const successAction = baseActionCreators.fetchSuccess(returned)
31 | log("successAction", successAction)
32 | dispatch(successAction)
33 | }, function(response) {
34 | log("rejection", response)
35 | // On rejection dispatch the error action
36 | const errorAction = baseActionCreators.fetchError(response)
37 | dispatch(errorAction)
38 | }).catch(function(err) {
39 | console.error(err.toString())
40 | })
41 |
42 | return promise
43 | }
44 | },
45 |
46 | create(todo) {
47 | return function(dispatch) {
48 | const cid = cuid()
49 | todo = r.merge(todo, {id: cid})
50 |
51 | const optimisticAction = baseActionCreators.createStart(todo)
52 | dispatch(optimisticAction)
53 |
54 | const url = "/todos"
55 | const promise = axios({
56 | url: url,
57 | method: "POST",
58 | data: todo,
59 | })
60 |
61 | promise.then(function(response) {
62 | // dispatch the success action
63 | const returned = response.data
64 | const successAction = baseActionCreators.createSuccess(returned, cid)
65 | dispatch(successAction)
66 | }, function(response) {
67 | // rejection
68 | // dispatch the error action
69 | const errorAction = baseActionCreators.createError(response, todo)
70 | dispatch(errorAction)
71 | }).catch(function(err) {
72 | console.error(err.toString())
73 | })
74 |
75 | return promise
76 |
77 | }
78 | },
79 |
80 | update(todo) {
81 | return function(dispatch) {
82 | const optimisticAction = baseActionCreators.updateStart(todo)
83 | dispatch(optimisticAction)
84 |
85 | const url = `/todos/${todo.id}`
86 | const promise = axios({
87 | url: url,
88 | method: "PATCH",
89 | data: todo,
90 | })
91 |
92 | promise.then(function(response) {
93 | // dispatch the success action
94 | const returned = response.data
95 | const successAction = baseActionCreators.updateSuccess(returned)
96 | dispatch(successAction)
97 | }, function(response) {
98 | // rejection
99 | // dispatch the error action
100 | const errorAction = baseActionCreators.updateError(response, todo)
101 | dispatch(errorAction)
102 | }).catch(function(err) {
103 | console.error(err.toString())
104 | })
105 |
106 | return promise
107 |
108 | }
109 | },
110 |
111 | delete(todo) {
112 | return function(dispatch) {
113 | const optimisticAction = baseActionCreators.deleteStart(todo)
114 | dispatch(optimisticAction)
115 |
116 | const url = `/todos/${todo.id}`
117 | const promise = axios({
118 | url: url,
119 | method: "DELETE",
120 | })
121 |
122 | promise.then(function(response) {
123 | // dispatch the success action
124 | const successAction = baseActionCreators.deleteSuccess(todo)
125 | dispatch(successAction)
126 | }, function(response) {
127 | // rejection
128 | // dispatch the error action
129 | const errorAction = baseActionCreators.deleteError(response, todo)
130 | dispatch(errorAction)
131 | }).catch(function(err) {
132 | console.error(err.toString())
133 | })
134 |
135 | return promise
136 | }
137 | },
138 |
139 | }
140 |
141 | actionCreators = r.merge(baseActionCreators, actionCreators)
142 |
143 | export default actionCreators
144 |
--------------------------------------------------------------------------------
/example/src/todos/fixture.js:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | "id": 1,
4 | "title": "Learn Elm",
5 | "done": true
6 | },
7 | {
8 | "id": 2,
9 | "title": "Learn Haskell"
10 | },
11 | {
12 | "id": 3,
13 | "title": "Learn Elixir",
14 | "done": true
15 | },
16 | {
17 | "id": 4,
18 | "title": "Learn Rust"
19 | }
20 | ]
21 |
--------------------------------------------------------------------------------
/example/src/todos/reducer.js:
--------------------------------------------------------------------------------
1 | import reduxCrud from "../../../dist/index"
2 | import bows from "bows"
3 |
4 | var baseReducers = reduxCrud.List.reducersFor("todos")
5 | var log = bows("todos-reducer")
6 |
7 | export default function reducer(state=[], action) {
8 | log(action)
9 |
10 | switch (action.type) {
11 | default:
12 | return baseReducers(state, action)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-crud",
3 | "version": "3.3.0",
4 | "description": "Redux CRUD",
5 | "main": "dist/index.js",
6 | "typings": "dist/index.d.ts",
7 | "scripts": {
8 | "build": "tsc",
9 | "test": "tsc && ava dist/**/*.test.js",
10 | "lint": "tslint src/**/*.ts",
11 | "format": "prettier --bracket-spacing=false --write 'src/**/*.ts'",
12 | "precommit": "lint-staged",
13 | "prepublish": "npm run build"
14 | },
15 | "lint-staged": {
16 | "src/**/*.ts": [
17 | "prettier --write --bracket-spacing=false",
18 | "git add"
19 | ]
20 | },
21 | "files": [
22 | "dist",
23 | "src"
24 | ],
25 | "repository": {
26 | "type": "git",
27 | "url": "https://github.com/Versent/redux-crud"
28 | },
29 | "keywords": [
30 | "redux",
31 | "crud"
32 | ],
33 | "author": "Versent",
34 | "license": "MIT",
35 | "bugs": {
36 | "url": "https://github.com/Versent/redux-crud/issues"
37 | },
38 | "homepage": "https://github.com/Versent/redux-crud",
39 | "devDependencies": {
40 | "ava": "^0.22.0",
41 | "husky": "^0.14.3",
42 | "lint-staged": "^4.2.1",
43 | "prettier": "^1.3.1",
44 | "testdouble": "^3.2.5",
45 | "tslint": "^5.7.0",
46 | "typescript": "^2.1.5"
47 | },
48 | "dependencies": {
49 | "action-names": "^0.4.0",
50 | "invariant": "^2.1.0",
51 | "ramda": "^0.24.1"
52 | },
53 | "ava": {
54 | "failFast": true
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Redux CRUD
2 |
3 | **Looking for a new mantainer**: See
4 |
5 | [ ](https://codeship.com/projects/97928)
6 |
7 | Redux CRUD is a convention driven way of building CRUD applications using Redux. After building several Flux applications we found that we always end up creating the same action types, actions and reducers for all our resources.
8 |
9 | Redux CRUD gives you a standard set of:
10 |
11 | - action types: e.g. `USER_UPDATE_SUCCESS`
12 | - actions: e.g. `updateSuccess`, `updateError`
13 | - reducers: for the action types above e.g. `updateSuccess`
14 |
15 | # Working with resources in Redux
16 |
17 | When building an app you might have resources like __`users`__, __`posts`__ and __`comments`__.
18 |
19 | You'll probably end up with action types for them like:
20 |
21 | - `USERS_FETCH_SUCCESS`
22 | - `POSTS_FETCH_SUCCESS`
23 | - `COMMENTS_FETCH_SUCCESS`
24 |
25 | And action creators like:
26 |
27 | - `users.fetchSuccess`
28 | - `posts.fetchSuccess`
29 | - `comments.fetchSuccess`
30 |
31 | There's obvious repetition there. Redux CRUD aims to remove this boilerplate by providing strong conventions on naming and processing data.
32 |
33 | ## Stores
34 |
35 | Redux-crud provides two stores:
36 |
37 | - __List__. A plain JS array. This preserves the order of records.
38 | - __Map__. A JS object where records are indexed by key. This provides faster writes and lookups.
39 |
40 | ## Docs
41 |
42 | ### [Actions](./docs/actions.md)
43 | ### [Reducers](./docs/reducers.md)
44 | ### [Using with Redux](./docs/redux.md)
45 | ### [Tips](./docs/tips.md)
46 |
47 | ## Testing
48 |
49 | ```
50 | npm test
51 | ```
52 |
53 | ## Example
54 |
55 | You can see [a basic example here](./example)
56 |
57 |
--------------------------------------------------------------------------------
/src/actionCreatorsFor.test.ts:
--------------------------------------------------------------------------------
1 | import test from "ava";
2 | import actionCreatorsFor from "./actionCreatorsFor";
3 |
4 | const error = {};
5 | const actionCreators = actionCreatorsFor("users");
6 | const subject = " actionCreatorsFor: ";
7 | const arrayRegEx = /Expected record not to be an array/;
8 |
9 | function makeUser() {
10 | return {
11 | id: 11
12 | };
13 | }
14 |
15 | function makeUsers() {
16 | return [makeUser()];
17 | }
18 |
19 | test(subject + "returns the actionCreators", function(t) {
20 | t.truthy(actionCreators.fetchStart);
21 | t.truthy(actionCreators.fetchSuccess);
22 | t.truthy(actionCreators.fetchError);
23 |
24 | t.truthy(actionCreators.createStart);
25 | t.truthy(actionCreators.createSuccess);
26 | t.truthy(actionCreators.createError);
27 |
28 | t.truthy(actionCreators.updateStart);
29 | t.truthy(actionCreators.updateSuccess);
30 | t.truthy(actionCreators.updateError);
31 |
32 | t.truthy(actionCreators.deleteStart);
33 | t.truthy(actionCreators.deleteSuccess);
34 | t.truthy(actionCreators.deleteError);
35 | });
36 |
37 | test(subject + "fetchStart", function(t) {
38 | var data = {foo: 1};
39 |
40 | var action = actionCreators.fetchStart(data);
41 |
42 | t.deepEqual(action.type, "USERS_FETCH_START");
43 | t.deepEqual(action.data, data, "has the data");
44 | });
45 |
46 | test(subject + "fetchSuccess", function(t) {
47 | var data = {foo: 1};
48 | var users = makeUsers();
49 |
50 | var action = actionCreators.fetchSuccess(users, data);
51 |
52 | t.deepEqual(action.type, "USERS_FETCH_SUCCESS");
53 | t.deepEqual(action.records, users, "has the user");
54 | t.deepEqual(action.data, data, "has the data");
55 |
56 | function withoutPayload() {
57 | actionCreators.fetchSuccess();
58 | }
59 | t.throws(withoutPayload, /Expected records/);
60 | });
61 |
62 | test(subject + "fetchError", function(t) {
63 | var data = {foo: 1};
64 |
65 | var action = actionCreators.fetchError(error, data);
66 |
67 | t.deepEqual(action.type, "USERS_FETCH_ERROR");
68 | t.deepEqual(action.error, error, "has the error");
69 | t.deepEqual(action.data, data, "has the data");
70 | });
71 |
72 | test(subject + "createStart", function(t) {
73 | var user = makeUser();
74 | var data = {foo: 1};
75 |
76 | var action = actionCreators.createStart(user, data);
77 |
78 | t.deepEqual(action.type, "USERS_CREATE_START");
79 | t.deepEqual(action.record, user, "has the user");
80 | t.deepEqual(action.data, data, "has the data");
81 |
82 | function withoutPayload() {
83 | actionCreators.createStart();
84 | }
85 | t.throws(withoutPayload, /Expected record/);
86 |
87 | // it expects single record
88 | function withArray() {
89 | actionCreators.createStart([]);
90 | }
91 | t.throws(withArray, arrayRegEx);
92 |
93 | // it expects a key on the record
94 | function withoutKey() {
95 | var user = {};
96 | actionCreators.createStart(user);
97 | }
98 | t.throws(withoutKey, /Expected record\.id in createStart/);
99 | });
100 |
101 | test(subject + "createSuccess", function(t) {
102 | var user = makeUser();
103 | var data = {foo: 1};
104 |
105 | var action = actionCreators.createSuccess(user, "abc", data);
106 |
107 | t.deepEqual(action.type, "USERS_CREATE_SUCCESS");
108 | t.deepEqual(action.record, user, "has the user");
109 | t.deepEqual(action.cid, "abc", "has the cid");
110 | t.deepEqual(action.data, data, "has the data");
111 |
112 | function withoutPayload() {
113 | actionCreators.createSuccess();
114 | }
115 | t.throws(withoutPayload, /Expected record/);
116 |
117 | // it expects one
118 | function withArray() {
119 | actionCreators.createSuccess([]);
120 | }
121 | t.throws(withArray, arrayRegEx);
122 | });
123 |
124 | test(subject + "createError", function(t) {
125 | var user = makeUser();
126 | var data = {foo: 1};
127 |
128 | var action = actionCreators.createError(error, user, data);
129 |
130 | t.deepEqual(action.type, "USERS_CREATE_ERROR");
131 | t.deepEqual(action.error, error);
132 | t.deepEqual(action.record, user, "has the user");
133 | t.deepEqual(action.data, data, "has the data");
134 |
135 | function withoutPayload() {
136 | actionCreators.createError(error);
137 | }
138 | t.throws(withoutPayload, /Expected record/);
139 |
140 | // it expects single record
141 | function withArray() {
142 | actionCreators.createError(error, []);
143 | }
144 | t.throws(withArray, arrayRegEx);
145 |
146 | function withoutKey() {
147 | var user = {};
148 | actionCreators.createError(error, user);
149 | }
150 | t.throws(withoutKey, /Expected record\.id in createError/);
151 | });
152 |
153 | test(subject + "updateStart", function(t) {
154 | var user = makeUser();
155 | var data = {foo: 1};
156 |
157 | var action = actionCreators.updateStart(user, data);
158 |
159 | t.deepEqual(action.type, "USERS_UPDATE_START");
160 | t.deepEqual(action.record, user, "has the user");
161 | t.deepEqual(action.data, data, "has the data");
162 |
163 | function withoutPayload() {
164 | actionCreators.updateStart();
165 | }
166 | t.throws(withoutPayload, /Expected record/);
167 |
168 | // it expects one
169 | function withArray() {
170 | actionCreators.updateStart([]);
171 | }
172 | t.throws(withArray, arrayRegEx);
173 | });
174 |
175 | test(subject + "updateSuccess", function(t) {
176 | var user = makeUser();
177 | var data = {foo: 1};
178 |
179 | var action = actionCreators.updateSuccess(user, data);
180 |
181 | t.deepEqual(action.type, "USERS_UPDATE_SUCCESS");
182 | t.deepEqual(action.record, user, "has the user");
183 | t.deepEqual(action.data, data, "has the data");
184 |
185 | function withoutPayload() {
186 | actionCreators.updateSuccess();
187 | }
188 | t.throws(withoutPayload, /Expected record/);
189 |
190 | // it expects one
191 | function withArray() {
192 | actionCreators.updateSuccess([]);
193 | }
194 | t.throws(withArray, arrayRegEx);
195 | });
196 |
197 | test(subject + "updateError", function(t) {
198 | var user = makeUser();
199 | var data = {foo: 1};
200 |
201 | var action = actionCreators.updateError(error, user, data);
202 |
203 | t.deepEqual(action.type, "USERS_UPDATE_ERROR");
204 | t.deepEqual(action.error, error);
205 | t.deepEqual(action.record, user, "has the user");
206 | t.deepEqual(action.data, data, "has the data");
207 |
208 | function withoutPayload() {
209 | actionCreators.updateError(error);
210 | }
211 | t.throws(withoutPayload, /Expected record/);
212 |
213 | // it expects one
214 | function withArray() {
215 | actionCreators.updateError(error, []);
216 | }
217 | t.throws(withArray, arrayRegEx);
218 | });
219 |
220 | test(subject + "deleteStart", function(t) {
221 | var user = makeUser();
222 | var data = {foo: 1};
223 |
224 | var action = actionCreators.deleteStart(user, data);
225 |
226 | t.deepEqual(action.type, "USERS_DELETE_START");
227 | t.deepEqual(action.record, user, "has the user");
228 | t.deepEqual(action.data, data, "has the data");
229 |
230 | function withoutPayload() {
231 | actionCreators.deleteStart();
232 | }
233 | t.throws(withoutPayload, /Expected record/);
234 |
235 | // it expects one
236 | function withArray() {
237 | actionCreators.deleteStart([]);
238 | }
239 | t.throws(withArray, arrayRegEx);
240 | });
241 |
242 | test(subject + "deleteSuccess", function(t) {
243 | var user = makeUser();
244 | var data = {foo: 1};
245 |
246 | var action = actionCreators.deleteSuccess(user, data);
247 |
248 | t.deepEqual(action.type, "USERS_DELETE_SUCCESS");
249 | t.deepEqual(action.record, user, "has the user");
250 | t.deepEqual(action.data, data, "has the data");
251 |
252 | function withoutPayload() {
253 | actionCreators.deleteSuccess();
254 | }
255 | t.throws(withoutPayload, /Expected record/);
256 |
257 | // it expects one
258 | function withArray() {
259 | actionCreators.deleteSuccess([]);
260 | }
261 | t.throws(withArray, arrayRegEx);
262 | });
263 |
264 | test(subject + "deleteError", function(t) {
265 | var user = makeUser();
266 | var data = {foo: 1};
267 |
268 | var action = actionCreators.deleteError(error, user, data);
269 |
270 | t.deepEqual(action.type, "USERS_DELETE_ERROR");
271 | t.deepEqual(action.error, error);
272 | t.deepEqual(action.record, user, "has the user");
273 | t.deepEqual(action.data, data, "has the data");
274 |
275 | function withoutPayload() {
276 | actionCreators.deleteError(error);
277 | }
278 | t.throws(withoutPayload, /Expected record/);
279 |
280 | // it expects one
281 | function withArray() {
282 | actionCreators.deleteError(error, []);
283 | }
284 | t.throws(withArray, arrayRegEx);
285 | });
286 |
--------------------------------------------------------------------------------
/src/actionCreatorsFor.ts:
--------------------------------------------------------------------------------
1 | import * as merge from "ramda/src/merge";
2 | import * as invariant from "invariant";
3 |
4 | import actionTypesFor from "./actionTypesFor";
5 | import assertNotArray from "./utils/assertNotArray";
6 | import constants from "./constants";
7 | import getDefaultConfig from "./getDefaultConfig";
8 |
9 | import {Config, ReducerName} from "./types";
10 |
11 | // const invariant = require("invariant")
12 |
13 | function actionCreatorsFor(resourceName: string, config?: Config) {
14 | if (resourceName == null)
15 | throw new Error("actionCreatorsFor: Expected resourceName");
16 |
17 | config = config || getDefaultConfig(resourceName);
18 | config = merge(config, {resourceName});
19 |
20 | const actionTypes = actionTypesFor(resourceName);
21 | const key = config.key || constants.DEFAULT_KEY;
22 |
23 | function assertError(actionCreatorName: ReducerName, error) {
24 | invariant(error != null, "Expected error in " + actionCreatorName);
25 | }
26 |
27 | function assertOneRecord(actionCreatorName: ReducerName, record?: any) {
28 | invariant(record != null, "Expected record in " + actionCreatorName);
29 | assertNotArray(config, "createStart", record);
30 | invariant(
31 | record[key] != null,
32 | "Expected record." + key + " in " + actionCreatorName
33 | );
34 | }
35 |
36 | function assertManyRecords(actionCreatorName, records) {
37 | invariant(records != null, "Expected records " + actionCreatorName);
38 | }
39 |
40 | return {
41 | fetchStart(data?) {
42 | return {
43 | data: data,
44 | type: actionTypes.fetchStart
45 | };
46 | },
47 |
48 | fetchSuccess(records?: T[], data?) {
49 | var name: ReducerName = "fetchSuccess";
50 | assertManyRecords(name, records);
51 |
52 | return {
53 | data: data,
54 | records: records,
55 | type: actionTypes.fetchSuccess
56 | };
57 | },
58 |
59 | fetchError(error?, data?) {
60 | var name: ReducerName = "fetchError";
61 | assertError(name, error);
62 |
63 | return {
64 | data: data,
65 | error: error,
66 | type: actionTypes.fetchError
67 | };
68 | },
69 |
70 | createStart(record?: T, data?) {
71 | var name: ReducerName = "createStart";
72 | assertOneRecord(name, record);
73 |
74 | return {
75 | data: data,
76 | record: record,
77 | type: actionTypes.createStart
78 | };
79 | },
80 |
81 | createSuccess(record?: T, clientGeneratedKey?, data?) {
82 | var name: ReducerName = "createSuccess";
83 | assertOneRecord(name, record);
84 |
85 | return {
86 | cid: clientGeneratedKey,
87 | data: data,
88 | record: record,
89 | type: actionTypes.createSuccess
90 | };
91 | },
92 |
93 | createError(error?, record?: T, data?) {
94 | var name: ReducerName = "createError";
95 | assertError(name, error);
96 | assertOneRecord(name, record);
97 |
98 | return {
99 | data: data,
100 | error: error,
101 | record: record,
102 | type: actionTypes.createError
103 | };
104 | },
105 |
106 | updateStart(record?: T, data?) {
107 | var name: ReducerName = "updateStart";
108 | assertOneRecord(name, record);
109 |
110 | return {
111 | data: data,
112 | record: record,
113 | type: actionTypes.updateStart
114 | };
115 | },
116 |
117 | updateSuccess(record?: T, data?) {
118 | var name: ReducerName = "updateSuccess";
119 | assertOneRecord(name, record);
120 |
121 | return {
122 | data: data,
123 | record: record,
124 | type: actionTypes.updateSuccess
125 | };
126 | },
127 |
128 | updateError(error?, record?: T, data?) {
129 | var name: ReducerName = "updateError";
130 | assertError(name, error);
131 | assertOneRecord(name, record);
132 |
133 | return {
134 | data: data,
135 | error: error,
136 | record: record,
137 | type: actionTypes.updateError
138 | };
139 | },
140 |
141 | deleteStart(record?: T, data?) {
142 | var name: ReducerName = "deleteStart";
143 | assertOneRecord(name, record);
144 |
145 | return {
146 | data: data,
147 | record: record,
148 | type: actionTypes.deleteStart
149 | };
150 | },
151 |
152 | deleteSuccess(record?: T, data?) {
153 | var name: ReducerName = "deleteSuccess";
154 | assertOneRecord(name, record);
155 |
156 | return {
157 | data: data,
158 | record: record,
159 | type: actionTypes.deleteSuccess
160 | };
161 | },
162 |
163 | deleteError(error?, record?: T, data?) {
164 | var name: ReducerName = "deleteError";
165 | assertError(name, error);
166 | assertOneRecord(name, record);
167 |
168 | return {
169 | data: data,
170 | error: error,
171 | record: record,
172 | type: actionTypes.deleteError
173 | };
174 | }
175 | };
176 | }
177 |
178 | export default actionCreatorsFor;
179 |
--------------------------------------------------------------------------------
/src/actionTypesFor.test.ts:
--------------------------------------------------------------------------------
1 | import actionTypesFor from "./actionTypesFor";
2 | import test from "ava";
3 |
4 | const actionTypes = actionTypesFor("users");
5 |
6 | test("returns the action actionTypes", function(t) {
7 | t.deepEqual(actionTypes.USERS_FETCH_START, "USERS_FETCH_START");
8 | t.deepEqual(actionTypes.USERS_FETCH_SUCCESS, "USERS_FETCH_SUCCESS");
9 | t.deepEqual(actionTypes.USERS_FETCH_ERROR, "USERS_FETCH_ERROR");
10 |
11 | t.deepEqual(actionTypes.USERS_UPDATE_START, "USERS_UPDATE_START");
12 | t.deepEqual(actionTypes.USERS_UPDATE_SUCCESS, "USERS_UPDATE_SUCCESS");
13 | t.deepEqual(actionTypes.USERS_UPDATE_ERROR, "USERS_UPDATE_ERROR");
14 |
15 | t.deepEqual(actionTypes.USERS_CREATE_START, "USERS_CREATE_START");
16 | t.deepEqual(actionTypes.USERS_CREATE_SUCCESS, "USERS_CREATE_SUCCESS");
17 | t.deepEqual(actionTypes.USERS_CREATE_ERROR, "USERS_CREATE_ERROR");
18 |
19 | t.deepEqual(actionTypes.USERS_DELETE_START, "USERS_DELETE_START");
20 | t.deepEqual(actionTypes.USERS_DELETE_SUCCESS, "USERS_DELETE_SUCCESS");
21 | t.deepEqual(actionTypes.USERS_DELETE_ERROR, "USERS_DELETE_ERROR");
22 | });
23 |
24 | test("returns aliases", function(t) {
25 | t.deepEqual(actionTypes.fetchStart, "USERS_FETCH_START");
26 | t.deepEqual(actionTypes.fetchSuccess, "USERS_FETCH_SUCCESS");
27 | t.deepEqual(actionTypes.fetchError, "USERS_FETCH_ERROR");
28 |
29 | t.deepEqual(actionTypes.updateStart, "USERS_UPDATE_START");
30 | t.deepEqual(actionTypes.updateSuccess, "USERS_UPDATE_SUCCESS");
31 | t.deepEqual(actionTypes.updateError, "USERS_UPDATE_ERROR");
32 |
33 | t.deepEqual(actionTypes.createStart, "USERS_CREATE_START");
34 | t.deepEqual(actionTypes.createSuccess, "USERS_CREATE_SUCCESS");
35 | t.deepEqual(actionTypes.createError, "USERS_CREATE_ERROR");
36 |
37 | t.deepEqual(actionTypes.deleteStart, "USERS_DELETE_START");
38 | t.deepEqual(actionTypes.deleteSuccess, "USERS_DELETE_SUCCESS");
39 | t.deepEqual(actionTypes.deleteError, "USERS_DELETE_ERROR");
40 | });
41 |
--------------------------------------------------------------------------------
/src/actionTypesFor.ts:
--------------------------------------------------------------------------------
1 | import * as actionTypesFor from "action-names";
2 |
3 | export default actionTypesFor;
4 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | import {ReducerName} from "./types";
2 |
3 | const CREATE_ERROR: ReducerName = "createError";
4 | const CREATE_START: ReducerName = "createStart";
5 | const CREATE_SUCCESS: ReducerName = "createSuccess";
6 | const DELETE_ERROR: ReducerName = "deleteError";
7 | const DELETE_START: ReducerName = "deleteStart";
8 | const DELETE_SUCCESS: ReducerName = "deleteSuccess";
9 | const FETCH_SUCCESS: ReducerName = "fetchSuccess";
10 | const UPDATE_ERROR: ReducerName = "updateError";
11 | const UPDATE_START: ReducerName = "updateStart";
12 | const UPDATE_SUCCESS: ReducerName = "updateSuccess";
13 |
14 | export default {
15 | DEFAULT_KEY: "id",
16 | STORE_LIST: "STORE_LIST",
17 | STORE_MAP: "STORE_MAP",
18 | REDUCER_NAMES: {
19 | CREATE_ERROR,
20 | CREATE_START,
21 | CREATE_SUCCESS,
22 | DELETE_ERROR,
23 | DELETE_START,
24 | DELETE_SUCCESS,
25 | FETCH_SUCCESS,
26 | UPDATE_ERROR,
27 | UPDATE_START,
28 | UPDATE_SUCCESS
29 | },
30 | SPECIAL_KEYS: {
31 | BUSY: "busy",
32 | CLIENT_GENERATED_ID: "_cid",
33 | DELETED: "deleted",
34 | PENDING_CREATE: "pendingCreate",
35 | PENDING_UPDATE: "pendingUpdate"
36 | }
37 | };
38 |
--------------------------------------------------------------------------------
/src/getDefaultConfig.ts:
--------------------------------------------------------------------------------
1 | import {Config} from "./types";
2 |
3 | export default function getDefaultConfig(resourceName: string): Config {
4 | return {
5 | key: "id",
6 | resourceName
7 | };
8 | }
9 |
--------------------------------------------------------------------------------
/src/index.test.ts:
--------------------------------------------------------------------------------
1 | import test from "ava";
2 | import * as is from "ramda/src/is";
3 | import index from "./index";
4 |
5 | test("it has the expected functions", function(t) {
6 | t.truthy(is(Function, index.actionCreatorsFor));
7 | t.truthy(is(Function, index.actionTypesFor));
8 | t.truthy(is(Object, index.List));
9 | t.truthy(is(Function, index.List.reducersFor));
10 | t.truthy(is(Object, index.Map));
11 | t.truthy(is(Function, index.Map.reducersFor));
12 | });
13 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import {Config} from "./types";
2 | import actionCreatorsFor from "./actionCreatorsFor";
3 | import actionTypesFor from "./actionTypesFor";
4 | import constants from "./constants";
5 | import List from "./reducers/list";
6 | import Map from "./reducers/map";
7 |
8 | export * from "./types";
9 |
10 | export default {
11 | actionCreatorsFor,
12 | actionTypesFor,
13 | constants,
14 | List,
15 | Map
16 | };
17 |
--------------------------------------------------------------------------------
/src/reducers/common/create/start.ts:
--------------------------------------------------------------------------------
1 | import * as merge from "ramda/src/merge"
2 | import constants from "../../../constants";
3 |
4 | export function prepareRecord(record: Object) {
5 | var recordStatus = {
6 | [constants.SPECIAL_KEYS.BUSY]: true,
7 | [constants.SPECIAL_KEYS.PENDING_CREATE]: true
8 | };
9 |
10 | return merge(record, recordStatus);
11 | }
12 |
--------------------------------------------------------------------------------
/src/reducers/common/delete/start.ts:
--------------------------------------------------------------------------------
1 | import * as merge from "ramda/src/merge"
2 | import constants from "../../../constants";
3 |
4 | export function prepareRecord(record: Object) {
5 | var recordStatus = {
6 | [constants.SPECIAL_KEYS.DELETED]: true,
7 | [constants.SPECIAL_KEYS.BUSY]: true
8 | };
9 |
10 | return merge(record, recordStatus);
11 | }
12 |
--------------------------------------------------------------------------------
/src/reducers/common/reducersFor.ts:
--------------------------------------------------------------------------------
1 | import * as merge from "ramda/src/merge"
2 |
3 | import actionTypesFor from "../../actionTypesFor";
4 | import constants from "../../constants";
5 |
6 | import {Config, ReducerName} from "../../types";
7 |
8 | function reducersFor(resourceName: string, args = {}, emptyState, reducers) {
9 | if (resourceName == null)
10 | throw new Error("reducersFor: Expected resourceName");
11 |
12 | var defaults = {
13 | key: constants.DEFAULT_KEY,
14 | resourceName: resourceName
15 | };
16 |
17 | var config = merge(defaults, args);
18 |
19 | return function getReducer(state, action) {
20 | state = state || emptyState;
21 |
22 | if (action == null)
23 | throw new Error(resourceName + " reducers: Expected action");
24 |
25 | var actionTypes = actionTypesFor(resourceName);
26 | var record = action.record;
27 |
28 | switch (action.type) {
29 | case actionTypes.fetchSuccess:
30 | return reducers.fetchSuccess(
31 | config,
32 | state,
33 | action.records,
34 | emptyState,
35 | action.data && action.data.replace
36 | );
37 |
38 | case actionTypes.createStart:
39 | return reducers.createStart(config, state, record);
40 |
41 | case actionTypes.createSuccess:
42 | return reducers.createSuccess(config, state, record, action.cid);
43 |
44 | case actionTypes.createError:
45 | return reducers.createError(config, state, record);
46 |
47 | case actionTypes.updateStart:
48 | return reducers.updateStart(config, state, record);
49 |
50 | case actionTypes.updateSuccess:
51 | return reducers.updateSuccess(config, state, record);
52 |
53 | case actionTypes.updateError:
54 | return reducers.updateError(config, state, record);
55 |
56 | case actionTypes.deleteStart:
57 | return reducers.deleteStart(config, state, record);
58 |
59 | case actionTypes.deleteSuccess:
60 | return reducers.deleteSuccess(config, state, record);
61 |
62 | case actionTypes.deleteError:
63 | return reducers.deleteError(config, state, record);
64 |
65 | default:
66 | return state;
67 | }
68 | };
69 | }
70 |
71 | export default reducersFor;
72 |
--------------------------------------------------------------------------------
/src/reducers/common/update/error.ts:
--------------------------------------------------------------------------------
1 | import * as dissoc from "ramda/src/dissoc"
2 | import constants from "../../../constants";
3 |
4 | export function prepareRecord(record: Object) {
5 | return dissoc(constants.SPECIAL_KEYS.BUSY, record);
6 | }
7 |
--------------------------------------------------------------------------------
/src/reducers/common/update/start.ts:
--------------------------------------------------------------------------------
1 | import * as merge from "ramda/src/merge"
2 | import constants from "../../../constants";
3 |
4 | export function prepareRecord(record: Object) {
5 | var recordStatus = {
6 | [constants.SPECIAL_KEYS.BUSY]: true,
7 | [constants.SPECIAL_KEYS.PENDING_UPDATE]: true
8 | };
9 |
10 | return merge(record, recordStatus);
11 | }
12 |
--------------------------------------------------------------------------------
/src/reducers/invariants.ts:
--------------------------------------------------------------------------------
1 | import assertHasKey from "./invariants/assertHasKey";
2 | import assertNotArray from "../utils/assertNotArray";
3 | import constants from "../constants";
4 | import makeScope from "../utils/makeScope";
5 | import wrapArray from "../utils/wrapArray";
6 |
7 | import {
8 | Config,
9 | InvariantsBaseArgs,
10 | InvariantsExtraArgs,
11 | ReducerName
12 | } from "../types";
13 |
14 | export default function invariants(
15 | baseArgs: InvariantsBaseArgs,
16 | extraArgs: InvariantsExtraArgs
17 | ) {
18 | var config = extraArgs.config;
19 |
20 | if (!config.resourceName) throw new Error("Expected config.resourceName");
21 |
22 | const scope = makeScope(config, baseArgs.reducerName);
23 |
24 | if (!config.key) throw new Error(scope + ": Expected config.key");
25 | if (!extraArgs.record) throw new Error(scope + ": Expected record/s");
26 |
27 | extraArgs.assertValidStore(scope, extraArgs.current);
28 |
29 | if (!baseArgs.canBeArray) {
30 | assertNotArray(extraArgs.config, baseArgs.reducerName, extraArgs.record);
31 | }
32 |
33 | assertHasKey(extraArgs.config, scope, extraArgs.record);
34 | }
35 |
--------------------------------------------------------------------------------
/src/reducers/invariants/assertHasKey.ts:
--------------------------------------------------------------------------------
1 | import * as forEach from "ramda/src/forEach"
2 |
3 | import constants from "../../constants";
4 | import wrapArray from "../../utils/wrapArray";
5 |
6 | import {Config, ReducerName} from "../../types";
7 |
8 | export default function assertHasKey(
9 | config: Config,
10 | scope: string,
11 | recordOrRecords: any
12 | ): void {
13 | var key = config.key;
14 | var records = wrapArray(recordOrRecords);
15 |
16 | forEach(function(record) {
17 | if (record[key] == null) {
18 | throw new Error(scope + ": Expected record to have ." + key);
19 | }
20 | })(records);
21 | }
22 |
--------------------------------------------------------------------------------
/src/reducers/list.ts:
--------------------------------------------------------------------------------
1 | import reducersFor from "./list/reducersFor";
2 |
3 | export default {
4 | reducersFor
5 | };
6 |
--------------------------------------------------------------------------------
/src/reducers/list/create/error.test.ts:
--------------------------------------------------------------------------------
1 | import test from "ava";
2 |
3 | import constants from "../../../constants";
4 | import reducer from "./error";
5 |
6 | var subject = constants.REDUCER_NAMES.CREATE_ERROR;
7 | var config = {
8 | key: constants.DEFAULT_KEY,
9 | resourceName: "users"
10 | };
11 |
12 | function getCurrent() {
13 | return [
14 | {
15 | id: 1,
16 | name: "Blue"
17 | },
18 | {
19 | id: "abc",
20 | name: "Green"
21 | }
22 | ];
23 | }
24 |
25 | test(subject + "throws if given an array", function(t) {
26 | var curr = getCurrent();
27 | var created = [];
28 |
29 | function fn() {
30 | reducer(config, curr, created);
31 | }
32 |
33 | t.throws(fn, TypeError);
34 | });
35 |
36 | test(subject + "removes the record", function(t) {
37 | var curr = getCurrent();
38 | var created = {
39 | id: "abc",
40 | name: "Green"
41 | };
42 | var updated = reducer(config, curr, created);
43 |
44 | t.deepEqual(updated.length, 1);
45 | });
46 |
--------------------------------------------------------------------------------
/src/reducers/list/create/error.ts:
--------------------------------------------------------------------------------
1 | import assertNotArray from "../../../utils/assertNotArray";
2 | import constants from "../../../constants";
3 | import invariants from "../invariants";
4 | import remove from "../store/remove";
5 |
6 | import {Config, InvariantsBaseArgs, ReducerName} from "../../../types";
7 |
8 | var reducerName: ReducerName = constants.REDUCER_NAMES.CREATE_ERROR;
9 | var invariantArgs: InvariantsBaseArgs = {
10 | reducerName,
11 | canBeArray: false
12 | };
13 |
14 | export default function error(
15 | config: Config,
16 | current: Array,
17 | record: any
18 | ): Array {
19 | invariants(invariantArgs, config, current, record);
20 |
21 | return remove(config, current, record);
22 | }
23 |
--------------------------------------------------------------------------------
/src/reducers/list/create/start.test.ts:
--------------------------------------------------------------------------------
1 | import constants from "../../../constants";
2 | import reducer from "./start";
3 | import test from "ava";
4 |
5 | var config = {
6 | key: constants.DEFAULT_KEY,
7 | resourceName: "users"
8 | };
9 | var subject = "createStart: ";
10 |
11 | function getCurrent() {
12 | return [
13 | {
14 | id: 1,
15 | name: "Blue"
16 | },
17 | {
18 | id: 2,
19 | name: "Red"
20 | }
21 | ];
22 | }
23 |
24 | function getValid() {
25 | return {
26 | id: 3,
27 | name: "Green"
28 | };
29 | }
30 |
31 | test(subject + "throws if given an array", function(t) {
32 | var curr = getCurrent();
33 | var created = [];
34 | function fn() {
35 | reducer(config, curr, created);
36 | }
37 |
38 | t.throws(fn, TypeError);
39 | });
40 |
41 | test(subject + "adds the new record", function(t) {
42 | var curr = getCurrent();
43 | var other = {
44 | id: 3,
45 | name: "Green"
46 | };
47 | var updated = reducer(config, curr, other);
48 |
49 | t.deepEqual(updated.length, 3, "adds the record");
50 | });
51 |
52 | test(subject + "it throws when record doesnt have an id", function(t) {
53 | var curr = getCurrent();
54 | var record = {
55 | name: "Green"
56 | };
57 |
58 | var f = function() {
59 | reducer(config, curr, record);
60 | };
61 | t.throws(f, /users.createStart: Expected record to have .id/);
62 | });
63 |
64 | test(subject + "adds busy and pendingCreate", function(t) {
65 | var curr = getCurrent();
66 | var record = getValid();
67 | var updated = reducer(config, curr, record);
68 |
69 | t.deepEqual(updated[2].name, "Green");
70 | t.truthy(updated[2].busy, "adds busy");
71 | t.truthy(updated[2].pendingCreate, "adds pendingCreate");
72 | });
73 |
--------------------------------------------------------------------------------
/src/reducers/list/create/start.ts:
--------------------------------------------------------------------------------
1 | import {prepareRecord} from "../../common/create/start";
2 | import constants from "../../../constants";
3 | import invariants from "../invariants";
4 | import store from "../store";
5 |
6 | import {Config, InvariantsBaseArgs, ReducerName} from "../../../types";
7 |
8 | var reducerName: ReducerName = constants.REDUCER_NAMES.CREATE_START;
9 | var invariantArgs: InvariantsBaseArgs = {
10 | reducerName,
11 | canBeArray: false
12 | };
13 |
14 | export default function start(
15 | config: Config,
16 | current: Array,
17 | record: any
18 | ): Array {
19 | invariants(invariantArgs, config, current, record);
20 |
21 | // mark record as unsaved and busy
22 | var newRecord = prepareRecord(record);
23 |
24 | return store.merge(current, newRecord, config.key);
25 | }
26 |
--------------------------------------------------------------------------------
/src/reducers/list/create/success.test.ts:
--------------------------------------------------------------------------------
1 | import constants from "../../../constants";
2 | import reducer from "./success";
3 | import test from "ava";
4 |
5 | var subject = "createSuccess: ";
6 | var config = {
7 | key: constants.DEFAULT_KEY,
8 | resourceName: "users"
9 | };
10 |
11 | function getCurrent() {
12 | return [
13 | {
14 | id: 1,
15 | name: "Blue"
16 | },
17 | {
18 | id: 2,
19 | name: "Red"
20 | }
21 | ];
22 | }
23 |
24 | test(subject + "it throws if it cannot find config.key", function(t) {
25 | var curr = getCurrent();
26 | var record = {};
27 | var config = {
28 | resourceName: "users"
29 | };
30 | var f = function() {
31 | reducer(config, curr, record);
32 | };
33 | t.throws(f, /users.createSuccess: Expected config.key/);
34 | });
35 |
36 | test(subject + "doesnt mutate the original collection", function(t) {
37 | var curr = getCurrent();
38 | var record = {
39 | id: 3,
40 | name: "Green"
41 | };
42 | var updated = reducer(config, curr, record);
43 |
44 | t.is(curr.length, 2);
45 | });
46 |
47 | test(subject + "throws if given an array", function(t) {
48 | var curr = getCurrent();
49 | var record = [];
50 | function fn() {
51 | reducer(config, curr, record);
52 | }
53 |
54 | t.throws(fn, TypeError);
55 | });
56 |
57 | test(subject + "adds the record", function(t) {
58 | var curr = getCurrent();
59 | var record = {
60 | id: 3,
61 | name: "Green"
62 | };
63 | var updated = reducer(config, curr, record);
64 |
65 | t.is(updated.length, 3);
66 | });
67 |
68 | test(subject + "merges if exists", function(t) {
69 | var curr = getCurrent();
70 | var record = {
71 | id: 2,
72 | name: "Green"
73 | };
74 | var updated = reducer(config, curr, record);
75 |
76 | t.is(updated.length, 2);
77 | t.is(updated[1].id, 2);
78 | t.is(updated[1].name, "Green");
79 | });
80 |
81 | test(subject + "uses the given key", function(t) {
82 | var config = {
83 | key: "_id",
84 | resourceName: "users"
85 | };
86 | var curr = [
87 | {
88 | _id: 2,
89 | name: "Blue"
90 | }
91 | ];
92 | var record = {
93 | _id: 2,
94 | name: "Green"
95 | };
96 | var updated = reducer(config, curr, record);
97 |
98 | t.is(updated.length, 1);
99 | });
100 |
101 | test(subject + "it throws when record doesnt have an id", function(t) {
102 | var curr = getCurrent();
103 | var record = {
104 | name: "Green"
105 | };
106 |
107 | var f = function() {
108 | reducer(config, curr, record);
109 | };
110 | t.throws(f, /users.createSuccess: Expected record to have .id/);
111 | });
112 |
113 | test(subject + "it uses the cid", function(t) {
114 | var cid = "abc";
115 | var curr = [
116 | {
117 | id: cid,
118 | name: "Blue"
119 | }
120 | ];
121 | var record = {
122 | id: 3,
123 | name: "Green"
124 | };
125 | var updated = reducer(config, curr, record, cid);
126 | t.is(updated.length, 1);
127 | });
128 |
129 | test(subject + " cleans the cid", function(t) {
130 | var cid = "abc";
131 | var curr = [
132 | {
133 | id: cid,
134 | name: "Blue"
135 | }
136 | ];
137 |
138 | var record = {
139 | id: 3,
140 | name: "Green"
141 | };
142 |
143 | var updated = reducer(config, curr, record, cid);
144 | var updatedRecord = updated[0];
145 |
146 | t.is(updatedRecord._cid, undefined);
147 | });
148 |
149 | test(subject + "removes busy and pendingCreate", function(t) {
150 | var curr = [
151 | {
152 | busy: true,
153 | id: 2,
154 | name: "Green",
155 | pendingCreate: true
156 | }
157 | ];
158 | var record = {
159 | id: 2,
160 | name: "Yellow"
161 | };
162 | var updated = reducer(config, curr, record);
163 |
164 | t.is(updated.length, 1);
165 | t.truthy(updated[0].busy == null, "removes busy");
166 | t.truthy(updated[0].pendingCreate == null, "removes pendingCreate");
167 | });
168 |
--------------------------------------------------------------------------------
/src/reducers/list/create/success.ts:
--------------------------------------------------------------------------------
1 | import constants from "../../../constants";
2 | import invariants from "../invariants";
3 |
4 | import {Config, InvariantsBaseArgs, ReducerName} from "../../../types";
5 |
6 | var reducerName: ReducerName = constants.REDUCER_NAMES.CREATE_SUCCESS;
7 | var invariantArgs: InvariantsBaseArgs = {
8 | reducerName,
9 | canBeArray: false
10 | };
11 |
12 | export default function success(
13 | config: Config,
14 | current: Array,
15 | addedRecord: any,
16 | clientGeneratedKey?: string
17 | ): Array {
18 | invariants(invariantArgs, config, current, addedRecord);
19 |
20 | var key = config.key;
21 | var done = false;
22 |
23 | // Update existing records
24 | var updatedCollection = current.map(function(record) {
25 | var recordKey = record[key];
26 | if (recordKey == null) throw new Error("Expected record to have " + key);
27 | var isSameKey = recordKey === addedRecord[key];
28 | var isSameClientGetKey =
29 | clientGeneratedKey != null && clientGeneratedKey === recordKey;
30 | if (isSameKey || isSameClientGetKey) {
31 | done = true;
32 | return addedRecord;
33 | } else {
34 | return record;
35 | }
36 | });
37 |
38 | // Add if not updated
39 | if (!done) {
40 | updatedCollection = updatedCollection.concat([addedRecord]);
41 | }
42 |
43 | return updatedCollection;
44 | }
45 |
--------------------------------------------------------------------------------
/src/reducers/list/delete/error.test.ts:
--------------------------------------------------------------------------------
1 | import constants from "../../../constants";
2 | import reducer from "./error";
3 | import test from "ava";
4 |
5 | var config = {
6 | key: constants.DEFAULT_KEY,
7 | resourceName: "users"
8 | };
9 | var subject = constants.REDUCER_NAMES.DELETE_ERROR;
10 |
11 | function getCurrent() {
12 | return [
13 | {
14 | id: 1,
15 | name: "Blue",
16 | deleted: true,
17 | busy: true
18 | },
19 | {
20 | id: 2,
21 | name: "Red",
22 | deleted: true,
23 | busy: true
24 | }
25 | ];
26 | }
27 |
28 | test(subject + "throws if given an array", function(t) {
29 | var curr = getCurrent();
30 | var record = [];
31 | function fn() {
32 | reducer(config, curr, record);
33 | }
34 |
35 | t.throws(fn, TypeError);
36 | });
37 |
38 | test(subject + "doesnt mutate", function(t) {
39 | var curr = getCurrent();
40 | var record = {
41 | id: 1
42 | };
43 | var updated = reducer(config, curr, record);
44 |
45 | t.is(curr[0].deleted, true);
46 | t.is(curr[0].busy, true);
47 | t.is(updated[0].deleted, undefined);
48 | t.is(updated[0].busy, undefined);
49 | });
50 |
51 | test(subject + "removes deleted and busy", function(t) {
52 | var curr = getCurrent();
53 | var record = {
54 | id: 1
55 | };
56 | var updated = reducer(config, curr, record);
57 |
58 | t.deepEqual(updated.length, 2, "doesnt remove record");
59 | t.truthy(updated[0].deleted == null, "removes deleted");
60 | t.truthy(updated[0].busy == null, "removes busy");
61 |
62 | t.truthy(updated[1].deleted, "doesnt removes deleted from others");
63 | t.truthy(updated[1].busy, "doesnt removes busy from others");
64 | });
65 |
66 | test(subject + "uses the given key", function(t) {
67 | var config = {
68 | key: "_id",
69 | resourceName: "users"
70 | };
71 | var curr = [
72 | {
73 | _id: 1,
74 | deleted: true,
75 | busy: true
76 | }
77 | ];
78 | var record = {
79 | _id: 1
80 | };
81 | var updated = reducer(config, curr, record);
82 |
83 | t.truthy(updated[0].deleted == null, "removes deleted");
84 | t.truthy(updated[0].busy == null, "removes busy");
85 | });
86 |
87 | test(subject + "it throws when record doesnt have an id", function(t) {
88 | var curr = getCurrent();
89 | var record = {
90 | name: "Green"
91 | };
92 |
93 | var f = function() {
94 | reducer(config, curr, record);
95 | };
96 | t.throws(f);
97 | });
98 |
--------------------------------------------------------------------------------
/src/reducers/list/delete/error.ts:
--------------------------------------------------------------------------------
1 | import * as omit from "ramda/src/omit"
2 |
3 | import constants from "../../../constants";
4 | import findByKey from "../../../utils/findByKey";
5 | import invariants from "../invariants";
6 | import store from "../store";
7 |
8 | import {Config, InvariantsBaseArgs, ReducerName} from "../../../types";
9 |
10 | var reducerName: ReducerName = constants.REDUCER_NAMES.DELETE_ERROR;
11 | var invariantArgs: InvariantsBaseArgs = {
12 | reducerName,
13 | canBeArray: false
14 | };
15 |
16 | export default function error(
17 | config: Config,
18 | current: Array,
19 | record: any
20 | ): Array {
21 | invariants(invariantArgs, config, current, record);
22 |
23 | var key = config.key;
24 | var deleteId = record[key];
25 | var deleteRecord = findByKey(current, key, deleteId);
26 | deleteRecord = omit(["deleted", "busy"], deleteRecord);
27 |
28 | return store.merge(current, deleteRecord, key);
29 | }
30 |
--------------------------------------------------------------------------------
/src/reducers/list/delete/start.test.ts:
--------------------------------------------------------------------------------
1 | import constants from "../../../constants";
2 | import reducer from "./start";
3 | import test from "ava";
4 |
5 | var config = {
6 | key: constants.DEFAULT_KEY,
7 | resourceName: "users"
8 | };
9 | var subject = "deleteStart: ";
10 |
11 | function getCurrent() {
12 | return [
13 | {
14 | id: 1,
15 | name: "Blue"
16 | },
17 | {
18 | id: 2,
19 | name: "Red"
20 | }
21 | ];
22 | }
23 |
24 | function getValid() {
25 | return {
26 | id: 1,
27 | name: "Green"
28 | };
29 | }
30 |
31 | test(subject + "throws if given an array", function(t) {
32 | var curr = getCurrent();
33 | var record = [];
34 | function fn() {
35 | reducer(config, curr, record);
36 | }
37 |
38 | t.throws(fn, TypeError);
39 | });
40 |
41 | test(subject + "marks record as deleted and busy", function(t) {
42 | var curr = getCurrent();
43 | var record = getValid();
44 | var updated = reducer(config, curr, record);
45 |
46 | t.is(updated[0].deleted, true);
47 | t.is(updated[0].busy, true);
48 |
49 | t.truthy(updated[1].deleted == null, "doesnt add deleted to others");
50 | t.truthy(updated[1].busy == null, "doesnt add busy to others");
51 | });
52 |
53 | test(subject + "doesnt mutate", function(t) {
54 | var curr = getCurrent();
55 | var record = getValid();
56 | var updated = reducer(config, curr, record);
57 |
58 | t.is(updated[0].deleted, true);
59 | t.is(curr[0]["deleted"], undefined);
60 | });
61 |
62 | test(subject + "uses the given key", function(t) {
63 | var config = {
64 | key: "_id",
65 | resourceName: "users"
66 | };
67 | var curr = [
68 | {
69 | _id: 1
70 | }
71 | ];
72 | var record = {
73 | _id: 1
74 | };
75 | var updated = reducer(config, curr, record);
76 |
77 | t.truthy(updated[0].deleted, "adds deleted");
78 | });
79 |
80 | test(subject + "it throws when record dont have an id", function(t) {
81 | var curr = getCurrent();
82 | var record = {
83 | name: "Green"
84 | };
85 |
86 | var f = function() {
87 | reducer(config, curr, record);
88 | };
89 | t.throws(f);
90 | });
91 |
--------------------------------------------------------------------------------
/src/reducers/list/delete/start.ts:
--------------------------------------------------------------------------------
1 | import {prepareRecord} from "../../common/delete/start";
2 | import constants from "../../../constants";
3 | import findByKey from "../../../utils/findByKey";
4 | import invariants from "../invariants";
5 | import store from "../store";
6 |
7 | import {Config, InvariantsBaseArgs, ReducerName} from "../../../types";
8 |
9 | var reducerName: ReducerName = constants.REDUCER_NAMES.DELETE_START;
10 | var invariantArgs: InvariantsBaseArgs = {
11 | reducerName,
12 | canBeArray: false
13 | };
14 |
15 | export default function start(
16 | config: Config,
17 | current: Array,
18 | record: any
19 | ): Array {
20 | invariants(invariantArgs, config, current, record);
21 |
22 | var key = config.key;
23 | var deleteId = record[key];
24 |
25 | var deleteRecord = findByKey(current, key, deleteId);
26 | deleteRecord = prepareRecord(deleteRecord);
27 |
28 | return store.merge(current, deleteRecord, key);
29 | }
30 |
--------------------------------------------------------------------------------
/src/reducers/list/delete/success.test.ts:
--------------------------------------------------------------------------------
1 | import constants from "../../../constants";
2 | import reducer from "./success";
3 | import test from "ava";
4 |
5 | var config = {
6 | key: constants.DEFAULT_KEY,
7 | resourceName: "users"
8 | };
9 | var subject = "deleteSuccess: ";
10 |
11 | function getCurrent() {
12 | return [
13 | {
14 | id: 1,
15 | name: "Blue"
16 | },
17 | {
18 | id: 2,
19 | name: "Red"
20 | }
21 | ];
22 | }
23 |
24 | function getValid() {
25 | return {
26 | id: 1,
27 | name: "Green"
28 | };
29 | }
30 |
31 | test(subject + "throws if given an array", function(t) {
32 | var curr = getCurrent();
33 | var record = [];
34 | function fn() {
35 | reducer(config, curr, record);
36 | }
37 |
38 | t.throws(fn, TypeError);
39 | });
40 |
41 | test(subject + "removes the record", function(t) {
42 | var curr = getCurrent();
43 | var record = getValid();
44 | var updated = reducer(config, curr, record);
45 |
46 | t.is(updated.length, 1, "removes the record");
47 | t.is(updated[0].id, 2);
48 | });
49 |
50 | test(subject + "doesnt mutate the original collection", function(t) {
51 | var curr = getCurrent();
52 | var record = getValid();
53 | var updated = reducer(config, curr, record);
54 |
55 | t.is(curr.length, 2);
56 | t.is(updated.length, 1);
57 | });
58 |
59 | test(subject + "uses the given key", function(t) {
60 | var config = {
61 | key: "_id",
62 | resourceName: "users"
63 | };
64 | var curr = [
65 | {
66 | _id: 1
67 | }
68 | ];
69 | var record = {
70 | _id: 1
71 | };
72 | var updated = reducer(config, curr, record);
73 |
74 | t.deepEqual(updated.length, 0, "removes the record");
75 | });
76 |
77 | test(subject + "it throws when record dont have an id", function(t) {
78 | var curr = getCurrent();
79 | var record = {
80 | name: "Green"
81 | };
82 |
83 | var f = function() {
84 | reducer(config, curr, record);
85 | };
86 | t.throws(f);
87 | });
88 |
--------------------------------------------------------------------------------
/src/reducers/list/delete/success.ts:
--------------------------------------------------------------------------------
1 | import * as reject from "ramda/src/reject"
2 |
3 | import invariants from "../invariants";
4 | import constants from "../../../constants";
5 |
6 | import {Config, InvariantsBaseArgs, ReducerName} from "../../../types";
7 |
8 | var reducerName: ReducerName = constants.REDUCER_NAMES.DELETE_SUCCESS;
9 | var invariantArgs: InvariantsBaseArgs = {
10 | reducerName,
11 | canBeArray: false
12 | };
13 |
14 | export default function success(
15 | config: Config,
16 | current: Array,
17 | record: any
18 | ): Array {
19 | invariants(invariantArgs, config, current, record);
20 |
21 | var key = config.key;
22 | var deleteId = record[key];
23 |
24 | function predicate(existingRecord) {
25 | return deleteId == existingRecord[key];
26 | }
27 |
28 | return reject(predicate, current);
29 | }
30 |
--------------------------------------------------------------------------------
/src/reducers/list/fetch/success.test.ts:
--------------------------------------------------------------------------------
1 | import constants from "../../../constants";
2 | import reducer from "./success";
3 | import test from "ava";
4 |
5 | var config = {
6 | key: constants.DEFAULT_KEY,
7 | resourceName: "users"
8 | };
9 | var subject = "fetchSuccess: ";
10 |
11 | function getCurrent() {
12 | return [
13 | {
14 | id: 1,
15 | name: "Blue"
16 | },
17 | {
18 | id: 2,
19 | name: "Red"
20 | }
21 | ];
22 | }
23 |
24 | test(subject + " adds the records", function(t) {
25 | var curr = getCurrent();
26 | var more = [
27 | {
28 | id: 3,
29 | name: "Green"
30 | }
31 | ];
32 | var updated = reducer(config, curr, more, []);
33 |
34 | t.is(updated.length, 3);
35 | });
36 |
37 | test(subject + " doesnt mutate the original collection", function(t) {
38 | var curr = getCurrent();
39 | var more = [
40 | {
41 | id: 3,
42 | name: "Green"
43 | }
44 | ];
45 | var updated = reducer(config, curr, more, []);
46 |
47 | t.is(curr.length, 2);
48 | t.is(updated.length, 3);
49 | });
50 |
51 | test(subject + " merges", function(t) {
52 | var curr = getCurrent();
53 | var more = [
54 | {
55 | id: 2,
56 | name: "Green"
57 | }
58 | ];
59 | var updated = reducer(config, curr, more, []);
60 |
61 | t.is(updated.length, 2);
62 | t.is(updated[1].id, 2);
63 | t.is(updated[1].name, "Green");
64 | });
65 |
66 | test(subject + " replaces", function(t) {
67 | const curr = getCurrent();
68 | const more = [
69 | {
70 | id: 2,
71 | name: "Green"
72 | }
73 | ];
74 | const updated = reducer(config, curr, more, [], true);
75 |
76 | t.is(updated.length, 1);
77 | t.is(updated[0].id, 2);
78 | t.is(updated[0].name, "Green");
79 | });
80 |
81 | test(subject + "preserves the order", function(t) {
82 | var curr = [];
83 | var more = [
84 | {
85 | id: 11,
86 | label: "Eleven"
87 | },
88 | {
89 | id: 7,
90 | label: "Sevent"
91 | }
92 | ];
93 | var updated = reducer(config, curr, more, []);
94 |
95 | t.is(updated.length, 2, "it has two");
96 | t.is(updated[0].id, 11, "it is in the right position");
97 | });
98 |
99 | test(subject + "uses the given key", function(t) {
100 | var config = {
101 | key: "_id",
102 | resourceName: "users"
103 | };
104 | var curr = [
105 | {
106 | _id: 2,
107 | name: "Blue"
108 | }
109 | ];
110 | var more = [
111 | {
112 | _id: 2,
113 | name: "Green"
114 | }
115 | ];
116 | var updated = reducer(config, curr, more, []);
117 |
118 | t.is(updated.length, 1);
119 | });
120 |
121 | test(subject + "it throws when records dont have an id", function(t) {
122 | var curr = getCurrent();
123 | var more = [
124 | {
125 | name: "Green"
126 | }
127 | ];
128 |
129 | var f = function() {
130 | reducer(config, curr, more, []);
131 | };
132 | t.throws(f);
133 | });
134 |
135 | test(subject + "can take one record", function(t) {
136 | var curr = getCurrent();
137 | var one = {
138 | id: 3,
139 | name: "Green"
140 | };
141 | var updated = reducer(config, curr, one, []);
142 |
143 | t.is(updated.length, 3);
144 | });
145 |
--------------------------------------------------------------------------------
/src/reducers/list/fetch/success.ts:
--------------------------------------------------------------------------------
1 | import assertAllHaveKeys from "../../../utils/assertAllHaveKeys";
2 | import constants from "../../../constants";
3 | import store from "../store";
4 | import wrapArray from "../../../utils/wrapArray";
5 | import invariants from "../invariants";
6 |
7 | import {Config, InvariantsBaseArgs, ReducerName} from "../../../types";
8 |
9 | const reducerName: ReducerName = constants.REDUCER_NAMES.FETCH_SUCCESS;
10 | const invariantArgs: InvariantsBaseArgs = {
11 | reducerName,
12 | canBeArray: true
13 | };
14 |
15 | export default function success(
16 | config: Config,
17 | current: any[],
18 | records: any,
19 | emptyState: any,
20 | replace: boolean = false
21 | ): any[] {
22 | invariants(invariantArgs, config, current, records);
23 |
24 | // wrap array
25 | records = wrapArray(records);
26 |
27 | // All given records must have a key
28 | assertAllHaveKeys(config, reducerName, records);
29 |
30 | return store.merge(replace ? emptyState : current, records, config.key);
31 | }
32 |
--------------------------------------------------------------------------------
/src/reducers/list/invariants.ts:
--------------------------------------------------------------------------------
1 | import invariants from "../invariants";
2 | import store from "./store";
3 |
4 | import {Config, InvariantsBaseArgs, ReducerName} from "../../types";
5 |
6 | export default function invariantsList(
7 | invariantArgs: InvariantsBaseArgs,
8 | config: Config,
9 | current: Array,
10 | record: any
11 | ) {
12 | var extra = {
13 | assertValidStore: store.assert,
14 | config,
15 | current,
16 | record
17 | };
18 | invariants(invariantArgs, extra);
19 | }
20 |
--------------------------------------------------------------------------------
/src/reducers/list/reducersFor.test.ts:
--------------------------------------------------------------------------------
1 | import test from "ava";
2 | import * as td from "testdouble";
3 |
4 | import constants from "../../constants";
5 | import reducersFor from "./reducersFor";
6 |
7 | const current = [{}];
8 | const user = {};
9 | const error = "";
10 | const config = {
11 | key: constants.DEFAULT_KEY,
12 | resourceName: "users"
13 | };
14 | const subject = "reducersFor: ";
15 |
16 | test(subject + "calls fetchSuccess", function(t) {
17 | const fetchSuccess = td.function();
18 | const reducers = reducersFor("users", {}, {fetchSuccess});
19 |
20 | var users = [user];
21 |
22 | reducers(current, {
23 | records: users,
24 | type: "USERS_FETCH_SUCCESS"
25 | });
26 |
27 | td.verify(fetchSuccess(config, current, users, [], undefined));
28 | t.pass();
29 | });
30 |
31 | test(subject + "calls fetchSuccess with replace", function(t) {
32 | const fetchSuccess = td.function();
33 | const reducers = reducersFor("users", {}, {fetchSuccess});
34 |
35 | var users = [user];
36 |
37 | reducers(current, {
38 | data: {replace: true},
39 | records: users,
40 | type: "USERS_FETCH_SUCCESS"
41 | });
42 |
43 | td.verify(fetchSuccess(config, current, users, [], true));
44 | t.pass();
45 | });
46 |
47 | test(subject + "calls createStart", function(t) {
48 | const createStart = td.function();
49 | const reducers = reducersFor("users", {}, {createStart});
50 |
51 | reducers(current, {
52 | record: user,
53 | type: "USERS_CREATE_START"
54 | });
55 |
56 | td.verify(createStart(config, current, user));
57 | t.pass();
58 | });
59 |
60 | test(subject + "calls createSuccess", function(t) {
61 | const createSuccess = td.function();
62 | const reducers = reducersFor("users", {}, {createSuccess});
63 |
64 | var cid = "abc";
65 |
66 | reducers(current, {
67 | record: user,
68 | type: "USERS_CREATE_SUCCESS",
69 | cid: cid
70 | });
71 |
72 | td.verify(createSuccess(config, current, user, cid));
73 | t.pass();
74 | });
75 |
76 | test(subject + "calls createError", function(t) {
77 | const createError = td.function();
78 | const reducers = reducersFor("users", {}, {createError});
79 |
80 | reducers(current, {
81 | error: error,
82 | record: user,
83 | type: "USERS_CREATE_ERROR"
84 | });
85 |
86 | td.verify(createError(config, current, user));
87 | t.pass();
88 | });
89 |
90 | test(subject + "calls updateStart", function(t) {
91 | const updateStart = td.function();
92 | const reducers = reducersFor("users", {}, {updateStart});
93 |
94 | reducers(current, {
95 | record: user,
96 | type: "USERS_UPDATE_START"
97 | });
98 |
99 | td.verify(updateStart(config, current, user));
100 | t.pass();
101 | });
102 |
103 | test(subject + "calls updateSuccess", function(t) {
104 | const updateSuccess = td.function();
105 | const reducers = reducersFor("users", {}, {updateSuccess});
106 |
107 | reducers(current, {
108 | record: user,
109 | type: "USERS_UPDATE_SUCCESS"
110 | });
111 |
112 | td.verify(updateSuccess(config, current, user));
113 | t.pass();
114 | });
115 |
116 | test(subject + "calls updateError", function(t) {
117 | const updateError = td.function();
118 | const reducers = reducersFor("users", {}, {updateError});
119 |
120 | reducers(current, {
121 | error: error,
122 | record: user,
123 | type: "USERS_UPDATE_ERROR"
124 | });
125 |
126 | td.verify(updateError(config, current, user));
127 | t.pass();
128 | });
129 |
130 | test(subject + "calls deleteStart", function(t) {
131 | const deleteStart = td.function();
132 | const reducers = reducersFor("users", {}, {deleteStart});
133 |
134 | reducers(current, {
135 | record: user,
136 | type: "USERS_DELETE_START"
137 | });
138 |
139 | td.verify(deleteStart(config, current, user));
140 | t.pass();
141 | });
142 |
143 | test(subject + "calls deleteSuccess", function(t) {
144 | const deleteSuccess = td.function();
145 | const reducers = reducersFor("users", {}, {deleteSuccess});
146 |
147 | reducers(current, {
148 | record: user,
149 | type: "USERS_DELETE_SUCCESS"
150 | });
151 |
152 | td.verify(deleteSuccess(config, current, user));
153 | t.pass();
154 | });
155 |
156 | test(subject + "calls deleteError", function(t) {
157 | const deleteError = td.function();
158 | const reducers = reducersFor("users", {}, {deleteError});
159 |
160 | reducers(current, {
161 | error: error,
162 | record: user,
163 | type: "USERS_DELETE_ERROR"
164 | });
165 |
166 | td.verify(deleteError(config, current, user));
167 | t.pass();
168 | });
169 |
170 | test(subject + "it passes the given key", function(t) {
171 | const createStart = td.function();
172 | const reducers = reducersFor("users", {key: "_id"}, {createStart});
173 |
174 | reducers(current, {
175 | record: user,
176 | type: "USERS_CREATE_START"
177 | });
178 |
179 | var expectedConfig = {
180 | key: "_id",
181 | resourceName: "users"
182 | };
183 |
184 | td.verify(createStart(expectedConfig, current, user));
185 | t.pass();
186 | });
187 |
188 | test(subject + "it doesnt mutate the config", function(t) {
189 | const config = {};
190 | reducersFor("users", config);
191 | reducersFor("monkeys", config);
192 |
193 | t.deepEqual(config, {});
194 | });
195 |
--------------------------------------------------------------------------------
/src/reducers/list/reducersFor.ts:
--------------------------------------------------------------------------------
1 | import * as merge from "ramda/src/merge"
2 |
3 | import actionTypesFor from "../../actionTypesFor";
4 | import constants from "../../constants";
5 | import commonReducersFor from "../common/reducersFor";
6 | import createError from "./create/error";
7 | import createStart from "./create/start";
8 | import createSuccess from "./create/success";
9 | import deleteError from "./delete/error";
10 | import deleteStart from "./delete/start";
11 | import deleteSuccess from "./delete/success";
12 | import fetchSuccess from "./fetch/success";
13 | import updateError from "./update/error";
14 | import updateStart from "./update/start";
15 | import updateSuccess from "./update/success";
16 | import {Config, ReducerName} from "../../types";
17 |
18 | const baseReducers = {
19 | createError,
20 | createStart,
21 | createSuccess,
22 | deleteError,
23 | deleteStart,
24 | deleteSuccess,
25 | fetchSuccess,
26 | updateError,
27 | updateStart,
28 | updateSuccess
29 | };
30 |
31 | export default function reducersFor(resourceName: string, args = {}, deps?) {
32 | const reducers = merge(baseReducers, deps);
33 | return commonReducersFor(resourceName, args, [], reducers);
34 | }
35 |
--------------------------------------------------------------------------------
/src/reducers/list/store.ts:
--------------------------------------------------------------------------------
1 | import {Config} from "../../types";
2 | import assert from "./store/assert";
3 | import remove from "./store/remove";
4 | import merge from "./store/merge";
5 |
6 | export default {
7 | assert,
8 | remove,
9 | merge
10 | };
11 |
--------------------------------------------------------------------------------
/src/reducers/list/store/assert.ts:
--------------------------------------------------------------------------------
1 | import * as is from "ramda/src/is"
2 |
3 | export default function assert(scope: string, current: Array): void {
4 | var isArray = is(Array, current);
5 | if (!isArray) throw new Error(scope + ": Expected current to be an array");
6 | }
7 |
--------------------------------------------------------------------------------
/src/reducers/list/store/merge.ts:
--------------------------------------------------------------------------------
1 | import wrapArray from "../../../utils/wrapArray";
2 |
3 | /*
4 | Replaces an existing record in a list
5 | Or adds if not there
6 | */
7 | export default function merge(current, records, key) {
8 | records = wrapArray(records);
9 | var recordMap = {};
10 | var indexMap = {};
11 | var newRecords = current.slice(0);
12 |
13 | current.forEach(function(record, index) {
14 | var recordKey = record[key];
15 | if (recordKey == null) throw new Error("Expected record to have " + key);
16 | recordMap[recordKey] = record;
17 | indexMap[recordKey] = index;
18 | });
19 |
20 | records.forEach(function(record) {
21 | var recordId = record[key];
22 | if (recordMap[recordId]) {
23 | newRecords[indexMap[recordId]] = record;
24 | } else {
25 | indexMap[recordId] = newRecords.length;
26 | newRecords.push(record);
27 | }
28 | recordMap[recordId] = record;
29 | });
30 |
31 | return newRecords;
32 | }
33 |
--------------------------------------------------------------------------------
/src/reducers/list/store/remove.ts:
--------------------------------------------------------------------------------
1 | import * as reject from "ramda/src/reject"
2 |
3 | import {Config} from "../../../types";
4 |
5 | export default function remove(
6 | config: Config,
7 | current: Array,
8 | addedRecord: any
9 | ): Array {
10 | var key = config.key;
11 |
12 | function predicate(record: any) {
13 | var recordKey = record[key];
14 | var isSameKey = addedRecord[key] === recordKey;
15 | return isSameKey;
16 | }
17 |
18 | return reject(predicate, current);
19 | }
20 |
--------------------------------------------------------------------------------
/src/reducers/list/update/error.test.ts:
--------------------------------------------------------------------------------
1 | import constants from "../../../constants";
2 | import reducer from "./error";
3 | import test from "ava";
4 |
5 | var config = {
6 | key: constants.DEFAULT_KEY,
7 | resourceName: "users"
8 | };
9 | var subject = constants.REDUCER_NAMES.UPDATE_ERROR;
10 |
11 | function getCurrent() {
12 | return [
13 | {
14 | id: 1,
15 | name: "Blue",
16 | busy: true,
17 | pendingUpdate: true
18 | },
19 | {
20 | id: 2,
21 | name: "Red",
22 | busy: true,
23 | pendingUpdate: true
24 | }
25 | ];
26 | }
27 |
28 | function getValid() {
29 | return {
30 | id: 2,
31 | name: "Green"
32 | };
33 | }
34 |
35 | test(subject + "throws if given an array", function(t) {
36 | var curr = getCurrent();
37 | var record = [];
38 | function fn() {
39 | reducer(config, curr, record);
40 | }
41 |
42 | t.throws(fn, TypeError);
43 | });
44 |
45 | test(subject + "doesnt add record if not there", function(t) {
46 | var curr = getCurrent();
47 | var record = {
48 | id: 3,
49 | name: "Green"
50 | };
51 | var updated = reducer(config, curr, record);
52 |
53 | t.is(updated.length, 2);
54 | });
55 |
56 | test(subject + "removes busy", function(t) {
57 | var curr = getCurrent();
58 | var record = getValid();
59 | var updated = reducer(config, curr, record);
60 |
61 | t.truthy(updated[0].busy, "doesnt remove on others");
62 | t.truthy(updated[1].busy == null, "removes busy");
63 | });
64 |
65 | test(subject + "doesnt mutate the original collection", function(t) {
66 | var curr = getCurrent();
67 | var record = getValid();
68 | var updated = reducer(config, curr, record);
69 |
70 | t.is(curr[1].busy, true);
71 | t.is(updated[1].busy, undefined);
72 | });
73 |
74 | test(subject + "doesnt remove pendingUpdate", function(t) {
75 | var curr = getCurrent();
76 | var record = getValid();
77 | var updated = reducer(config, curr, record);
78 |
79 | t.truthy(updated[1].pendingUpdate);
80 | });
81 |
82 | test(subject + "uses the given key", function(t) {
83 | var config = {
84 | key: "_id",
85 | resourceName: "users"
86 | };
87 | var curr = [
88 | {
89 | _id: 2,
90 | name: "Blue",
91 | busy: true,
92 | unsaved: true
93 | }
94 | ];
95 | var record = {
96 | _id: 2
97 | };
98 | var updated = reducer(config, curr, record);
99 |
100 | t.truthy(updated[0].busy == null, "removes busy");
101 | });
102 |
103 | test(subject + "it throws when record dont have an id", function(t) {
104 | var curr = getCurrent();
105 | var record = {
106 | name: "Green"
107 | };
108 |
109 | var f = function() {
110 | reducer(config, curr, record);
111 | };
112 |
113 | t.throws(f);
114 | });
115 |
--------------------------------------------------------------------------------
/src/reducers/list/update/error.ts:
--------------------------------------------------------------------------------
1 | import {prepareRecord} from "../../common/update/error";
2 | import constants from "../../../constants";
3 | import findByKey from "../../../utils/findByKey";
4 | import invariants from "../invariants";
5 | import store from "../store";
6 |
7 | import {Config, InvariantsBaseArgs, ReducerName} from "../../../types";
8 |
9 | var reducerName: ReducerName = constants.REDUCER_NAMES.UPDATE_ERROR;
10 | var invariantArgs: InvariantsBaseArgs = {
11 | reducerName,
12 | canBeArray: false
13 | };
14 |
15 | export default function error(
16 | config: Config,
17 | current: Array,
18 | record: any
19 | ): Array {
20 | invariants(invariantArgs, config, current, record);
21 |
22 | // We don"t want to rollback
23 | var key = config.key;
24 | var updatedId = record[key];
25 | var updatedRecord = findByKey(current, key, updatedId);
26 |
27 | if (updatedRecord == null) return current;
28 |
29 | updatedRecord = prepareRecord(updatedRecord);
30 |
31 | return store.merge(current, updatedRecord, key);
32 | }
33 |
--------------------------------------------------------------------------------
/src/reducers/list/update/start.test.ts:
--------------------------------------------------------------------------------
1 | import constants from "../../../constants";
2 | import reducer from "./start";
3 | import test from "ava";
4 |
5 | var config = {
6 | key: constants.DEFAULT_KEY,
7 | resourceName: "users"
8 | };
9 | var subject = constants.REDUCER_NAMES.UPDATE_START;
10 |
11 | function getCurrent() {
12 | return [
13 | {
14 | id: 1,
15 | name: "Blue"
16 | },
17 | {
18 | id: 2,
19 | name: "Red"
20 | }
21 | ];
22 | }
23 |
24 | function getValid() {
25 | return {
26 | id: 2,
27 | name: "Green"
28 | };
29 | }
30 |
31 | test(subject + "throws if given an array", function(t) {
32 | var curr = getCurrent();
33 | var record = [];
34 | function fn() {
35 | reducer(config, curr, record);
36 | }
37 |
38 | t.throws(fn, TypeError);
39 | });
40 |
41 | test(subject + "adds the record if not there", function(t) {
42 | var curr = getCurrent();
43 | var record = {
44 | id: 3,
45 | name: "Green"
46 | };
47 | var updated = reducer(config, curr, record);
48 |
49 | t.is(updated.length, 3);
50 | });
51 |
52 | test(subject + "doesnt mutate the original", function(t) {
53 | var curr = getCurrent();
54 | var record = {
55 | id: 3,
56 | name: "Green"
57 | };
58 | var updated = reducer(config, curr, record);
59 |
60 | t.is(curr.length, 2);
61 | t.is(updated.length, 3);
62 | });
63 |
64 | test(subject + "updates existing", function(t) {
65 | var curr = getCurrent();
66 | var record = getValid();
67 | var updated = reducer(config, curr, record);
68 |
69 | t.is(updated.length, 2);
70 | t.is(updated[1].id, 2);
71 | t.is(updated[1].name, "Green");
72 | });
73 |
74 | test(subject + "uses the given key", function(t) {
75 | var config = {
76 | key: "_id",
77 | resourceName: "users"
78 | };
79 | var curr = [
80 | {
81 | _id: 2,
82 | name: "Blue"
83 | }
84 | ];
85 | var record = {
86 | _id: 2,
87 | name: "Green"
88 | };
89 | var updated = reducer(config, curr, record);
90 |
91 | t.is(updated.length, 1);
92 | });
93 |
94 | test(subject + "it throws when record dont have an id", function(t) {
95 | var curr = getCurrent();
96 | var record = {
97 | name: "Green"
98 | };
99 |
100 | var f = function() {
101 | reducer(config, curr, record);
102 | };
103 | t.throws(f);
104 | });
105 |
106 | test(subject + "adds busy and pendingUpdate", function(t) {
107 | var curr = getCurrent();
108 | var record = getValid();
109 | var updated = reducer(config, curr, record);
110 |
111 | t.deepEqual(updated[1].name, "Green");
112 | t.truthy(updated[1].busy, "adds busy");
113 | t.truthy(updated[1].pendingUpdate, "adds pendingUpdate");
114 | });
115 |
--------------------------------------------------------------------------------
/src/reducers/list/update/start.ts:
--------------------------------------------------------------------------------
1 | import {prepareRecord} from "../../common/update/start";
2 | import constants from "../../../constants";
3 | import invariants from "../invariants";
4 | import store from "../store";
5 |
6 | import {Config, InvariantsBaseArgs, ReducerName} from "../../../types";
7 |
8 | var reducerName: ReducerName = constants.REDUCER_NAMES.UPDATE_START;
9 | var invariantArgs: InvariantsBaseArgs = {
10 | reducerName,
11 | canBeArray: false
12 | };
13 |
14 | export default function start(
15 | config: Config,
16 | current: Array,
17 | record: any
18 | ): Array {
19 | invariants(invariantArgs, config, current, record);
20 |
21 | // mark record as unsaved and busy
22 | var newRecord = prepareRecord(record);
23 |
24 | // replace record
25 | return store.merge(current, newRecord, config.key);
26 | }
27 |
--------------------------------------------------------------------------------
/src/reducers/list/update/success.test.ts:
--------------------------------------------------------------------------------
1 | import constants from "../../../constants";
2 | import reducer from "./success";
3 | import test from "ava";
4 |
5 | var config = {
6 | key: constants.DEFAULT_KEY,
7 | resourceName: "users"
8 | };
9 |
10 | var subject = constants.REDUCER_NAMES.UPDATE_SUCCESS;
11 |
12 | function getCurrent() {
13 | return [
14 | {
15 | id: 1,
16 | name: "Blue",
17 | unsaved: true,
18 | busy: true
19 | },
20 | {
21 | id: 2,
22 | name: "Red",
23 | unsaved: true,
24 | busy: true
25 | }
26 | ];
27 | }
28 |
29 | function getValid() {
30 | return {
31 | id: 2,
32 | name: "Green"
33 | };
34 | }
35 |
36 | test(subject + "throws if given an array", function(t) {
37 | var curr = getCurrent();
38 | var record = [];
39 | function fn() {
40 | reducer(config, curr, record);
41 | }
42 |
43 | t.throws(fn, TypeError);
44 | });
45 |
46 | test(subject + "adds the record if not there", function(t) {
47 | var curr = getCurrent();
48 | var record = {
49 | id: 3,
50 | name: "Green"
51 | };
52 | var updated = reducer(config, curr, record);
53 |
54 | t.is(updated.length, 3);
55 | });
56 |
57 | test(subject + "doesnt mutate the original collection", function(t) {
58 | var curr = getCurrent();
59 | var record = {
60 | id: 3,
61 | name: "Green"
62 | };
63 | var updated = reducer(config, curr, record);
64 |
65 | t.is(curr.length, 2);
66 | t.is(updated.length, 3);
67 | });
68 |
69 | test(subject + "updates existing", function(t) {
70 | var curr = getCurrent();
71 | var record = getValid();
72 | var updated = reducer(config, curr, record);
73 |
74 | t.is(updated.length, 2);
75 | t.is(updated[1].id, 2);
76 | t.is(updated[1].name, "Green");
77 | });
78 |
79 | test(subject + "uses the given key", function(t) {
80 | var config = {
81 | key: "_id",
82 | resourceName: "users"
83 | };
84 | var curr = [
85 | {
86 | _id: 2,
87 | name: "Blue"
88 | }
89 | ];
90 | var record = {
91 | _id: 2,
92 | name: "Green"
93 | };
94 | var updated = reducer(config, curr, record);
95 |
96 | t.is(updated.length, 1);
97 | });
98 |
99 | test(subject + "it throws when record dont have an id", function(t) {
100 | var curr = getCurrent();
101 | var record = {
102 | name: "Green"
103 | };
104 |
105 | var f = function() {
106 | reducer(config, curr, record);
107 | };
108 | t.throws(f);
109 | });
110 |
111 | test(subject + "removes busy and pendingUpdate", function(t) {
112 | var curr = [
113 | {
114 | id: 2,
115 | name: "Green",
116 | pendingUpdate: true,
117 | busy: true
118 | }
119 | ];
120 | var record = getValid();
121 | var updated = reducer(config, curr, record);
122 |
123 | t.deepEqual(updated.length, 1);
124 | t.truthy(updated[0].busy == null, "removes busy");
125 | t.truthy(updated[0].pendingUpdate == null, "removes pendingUpdate");
126 | });
127 |
--------------------------------------------------------------------------------
/src/reducers/list/update/success.ts:
--------------------------------------------------------------------------------
1 | import constants from "../../../constants";
2 | import invariants from "../invariants";
3 | import store from "../store";
4 |
5 | import {Config, InvariantsBaseArgs, ReducerName} from "../../../types";
6 |
7 | var reducerName: ReducerName = constants.REDUCER_NAMES.UPDATE_SUCCESS;
8 | var invariantArgs: InvariantsBaseArgs = {
9 | reducerName,
10 | canBeArray: false
11 | };
12 |
13 | export default function success(
14 | config: Config,
15 | current: Array,
16 | record: any
17 | ): Array {
18 | invariants(invariantArgs, config, current, record);
19 |
20 | return store.merge(current, record, config.key);
21 | }
22 |
--------------------------------------------------------------------------------
/src/reducers/map.ts:
--------------------------------------------------------------------------------
1 | import reducersFor from "./map/reducersFor";
2 |
3 | export default {
4 | reducersFor
5 | };
6 |
--------------------------------------------------------------------------------
/src/reducers/map/create/error.test.ts:
--------------------------------------------------------------------------------
1 | import * as values from "ramda/src/values"
2 | import test from "ava";
3 |
4 | import constants from "../../../constants";
5 | import reducer from "./error";
6 |
7 | var subject = constants.REDUCER_NAMES.CREATE_ERROR;
8 | var config = {
9 | key: constants.DEFAULT_KEY,
10 | resourceName: "users"
11 | };
12 |
13 | function getCurrent() {
14 | return {
15 | 1: {
16 | id: 1,
17 | name: "Blue"
18 | },
19 | 2: {
20 | id: "abc",
21 | name: "Green"
22 | }
23 | };
24 | }
25 |
26 | test(subject + "throws if given an array", function(t) {
27 | var curr = getCurrent();
28 | var created = [];
29 |
30 | function fn() {
31 | reducer(config, curr, created);
32 | }
33 |
34 | t.throws(fn, TypeError);
35 | });
36 |
37 | test(subject + "removes the record", function(t) {
38 | var curr = getCurrent();
39 | t.deepEqual(values(curr).length, 2);
40 |
41 | var created = {
42 | id: "abc",
43 | name: "Green"
44 | };
45 | var updated = reducer(config, curr, created);
46 |
47 | t.deepEqual(values(updated).length, 2);
48 | });
49 |
--------------------------------------------------------------------------------
/src/reducers/map/create/error.ts:
--------------------------------------------------------------------------------
1 | import constants from "../../../constants";
2 | import invariants from "../invariants";
3 | import store from "../store";
4 |
5 | import {Config, InvariantsBaseArgs, Map, ReducerName} from "../../../types";
6 |
7 | var reducerName: ReducerName = constants.REDUCER_NAMES.CREATE_ERROR;
8 | var invariantArgs: InvariantsBaseArgs = {
9 | reducerName,
10 | canBeArray: false
11 | };
12 |
13 | export default function error(
14 | config: Config,
15 | current: Map,
16 | record: any
17 | ): Map {
18 | invariants(invariantArgs, config, current, record);
19 |
20 | return store.remove(config, current, record);
21 | }
22 |
--------------------------------------------------------------------------------
/src/reducers/map/create/start.test.ts:
--------------------------------------------------------------------------------
1 | import * as values from "ramda/src/values"
2 | import test from "ava";
3 |
4 | import constants from "../../../constants";
5 | import reducer from "./start";
6 |
7 | var config = {
8 | key: constants.DEFAULT_KEY,
9 | resourceName: "users"
10 | };
11 | var subject = constants.REDUCER_NAMES.CREATE_START;
12 |
13 | function getCurrent() {
14 | return {
15 | 1: {
16 | id: 1,
17 | name: "Blue"
18 | },
19 | 2: {
20 | id: "abc",
21 | name: "Green"
22 | }
23 | };
24 | }
25 |
26 | function getValid() {
27 | return {
28 | id: 3,
29 | name: "Green"
30 | };
31 | }
32 |
33 | test(subject + " throws if given an array", function(t) {
34 | var curr = getCurrent();
35 | var created = [];
36 | function fn() {
37 | reducer(config, curr, created);
38 | }
39 |
40 | t.throws(fn, TypeError);
41 | });
42 |
43 | test(subject + " adds the new record", function(t) {
44 | var curr = getCurrent();
45 |
46 | var other = {
47 | id: 3,
48 | name: "Green"
49 | };
50 | var updated = reducer(config, curr, other);
51 |
52 | t.is(values(updated).length, 3, "adds the record");
53 | });
54 |
55 | test(subject + "it throws when record doesnt have an id", function(t) {
56 | var curr = getCurrent();
57 | var record = {
58 | name: "Green"
59 | };
60 |
61 | var f = function() {
62 | reducer(config, curr, record);
63 | };
64 | t.throws(f, /users.createStart: Expected record to have .id/);
65 | });
66 |
67 | test(subject + "adds busy and pendingCreate", function(t) {
68 | var curr = getCurrent();
69 | var record = getValid();
70 | var updated = reducer(config, curr, record);
71 |
72 | t.is(updated["3"].name, "Green");
73 | t.truthy(updated["3"].busy, "adds busy");
74 | t.truthy(updated["3"].pendingCreate, "adds pendingCreate");
75 | });
76 |
--------------------------------------------------------------------------------
/src/reducers/map/create/start.ts:
--------------------------------------------------------------------------------
1 | import {prepareRecord} from "../../common/create/start";
2 | import constants from "../../../constants";
3 | import invariants from "../invariants";
4 | import store from "../store";
5 |
6 | import {Config, InvariantsBaseArgs, Map, ReducerName} from "../../../types";
7 |
8 | var reducerName: ReducerName = constants.REDUCER_NAMES.CREATE_START;
9 | var invariantArgs: InvariantsBaseArgs = {
10 | reducerName,
11 | canBeArray: false
12 | };
13 |
14 | export default function start(
15 | config: Config,
16 | current: Map,
17 | record: any
18 | ): Map {
19 | invariants(invariantArgs, config, current, record);
20 |
21 | // mark record as unsaved and busy
22 | var newRecord = prepareRecord(record);
23 |
24 | return store.merge(config, current, newRecord);
25 | }
26 |
--------------------------------------------------------------------------------
/src/reducers/map/create/success.test.ts:
--------------------------------------------------------------------------------
1 | import * as values from "ramda/src/values"
2 | import * as keys from "ramda/src/keys"
3 | import test from "ava";
4 |
5 | import constants from "../../../constants";
6 | import reducer from "./success";
7 |
8 | var subject = constants.REDUCER_NAMES.CREATE_SUCCESS;
9 | var config = {
10 | key: constants.DEFAULT_KEY,
11 | resourceName: "users"
12 | };
13 |
14 | function getCurrent() {
15 | return {
16 | 1: {
17 | id: 1,
18 | name: "Blue"
19 | },
20 | 2: {
21 | id: "abc",
22 | name: "Green"
23 | }
24 | };
25 | }
26 |
27 | test(subject + " it throws if it cannot find config.key", function(t) {
28 | var curr = getCurrent();
29 | var record = {};
30 | var config = {
31 | resourceName: "users"
32 | };
33 | var f = function() {
34 | reducer(config, curr, record);
35 | };
36 | t.throws(f, /users.createSuccess: Expected config.key/);
37 | });
38 |
39 | test(subject + " doesn't mutate the original collection", function(t) {
40 | var curr = getCurrent();
41 | var record = {
42 | id: 3,
43 | name: "Green"
44 | };
45 | var updated = reducer(config, curr, record);
46 |
47 | t.is(values(updated).length, 3);
48 | t.is(values(curr).length, 2);
49 | });
50 |
51 | test(subject + " throws if given an array", function(t) {
52 | var curr = getCurrent();
53 | var record = [];
54 | function fn() {
55 | reducer(config, curr, record);
56 | }
57 |
58 | t.throws(fn, TypeError);
59 | });
60 |
61 | test(subject + " adds the record", function(t) {
62 | var curr = getCurrent();
63 | var record = {
64 | id: 3,
65 | name: "Green"
66 | };
67 | var updated = reducer(config, curr, record);
68 | var actual = keys(updated);
69 | var expected = ["1", "2", "3"];
70 |
71 | t.deepEqual(actual, expected);
72 | });
73 |
74 | test(subject + " doesn't mutate the given record", function(t) {
75 | var curr = getCurrent();
76 |
77 | function getRecord() {
78 | return {
79 | busy: true,
80 | id: 3,
81 | name: "Green"
82 | };
83 | }
84 | var original = getRecord();
85 | var expected = getRecord();
86 |
87 | var updated = reducer(config, curr, original);
88 |
89 | t.deepEqual(original, expected);
90 | });
91 |
92 | test(subject + " merges if exists", function(t) {
93 | var curr = getCurrent();
94 | var record = {
95 | id: 2,
96 | name: "Green"
97 | };
98 | var updated = reducer(config, curr, record);
99 |
100 | t.is(values(updated).length, 2);
101 | t.is(updated["2"].id, 2);
102 | t.is(updated["2"].name, "Green");
103 | });
104 |
105 | test(subject + " uses the given key", function(t) {
106 | var config = {
107 | key: "_id",
108 | resourceName: "users"
109 | };
110 | var curr = {
111 | 2: {
112 | _id: 2,
113 | name: "Blue"
114 | }
115 | };
116 | var record = {
117 | _id: 2,
118 | name: "Green"
119 | };
120 |
121 | var updated = reducer(config, curr, record);
122 |
123 | t.is(values(updated).length, 1);
124 | });
125 |
126 | test(subject + " it throws when record doesn't have an id", function(t) {
127 | var curr = getCurrent();
128 | var record = {
129 | name: "Green"
130 | };
131 |
132 | var f = function() {
133 | reducer(config, curr, record);
134 | };
135 | t.throws(f, /users.createSuccess: Expected record to have .id/);
136 | });
137 |
138 | test(subject + " uses the cid to merge the record", function(t) {
139 | var cid = "abc";
140 | var curr = {
141 | [cid]: {
142 | id: cid,
143 | name: "Green"
144 | }
145 | };
146 | var record = {
147 | id: 3,
148 | name: "Green"
149 | };
150 |
151 | var updated = reducer(config, curr, record, cid);
152 |
153 | // It has the expected key
154 | var expectedKeys = ["3"];
155 | var actualKeys = keys(updated);
156 | t.deepEqual(actualKeys, expectedKeys);
157 |
158 | // It merged the record
159 | t.deepEqual(updated["3"], record);
160 | });
161 |
162 | test(subject + " cleans the cid", function(t) {
163 | var cid = "abc";
164 | var curr = {
165 | [cid]: {
166 | id: cid,
167 | name: "Green"
168 | }
169 | };
170 |
171 | var record = {
172 | id: 3,
173 | name: "Green"
174 | };
175 |
176 | var updated = reducer(config, curr, record, cid);
177 | var updatedRecord = updated["3"];
178 |
179 | t.is(updatedRecord._cid, undefined);
180 | });
181 |
182 | test(subject + " removes busy and pendingCreate", function(t) {
183 | var curr = {
184 | 2: {
185 | busy: true,
186 | id: 2,
187 | name: "Green",
188 | pendingCreate: true
189 | }
190 | };
191 | var record = {
192 | id: 2,
193 | name: "Yellow"
194 | };
195 | var updated = reducer(config, curr, record);
196 |
197 | t.is(values(updated).length, 1);
198 | t.truthy(updated["2"].busy == null, "removes busy");
199 | t.truthy(updated["2"].pendingCreate == null, "removes pendingCreate");
200 | });
201 |
--------------------------------------------------------------------------------
/src/reducers/map/create/success.ts:
--------------------------------------------------------------------------------
1 | import * as dissoc from "ramda/src/dissoc"
2 | import * as lensProp from "ramda/src/lensProp"
3 | import * as set from "ramda/src/set"
4 |
5 | import constants from "../../../constants";
6 | import invariants from "../invariants";
7 |
8 | import {Config, InvariantsBaseArgs, Map, ReducerName} from "../../../types";
9 |
10 | var reducerName: ReducerName = constants.REDUCER_NAMES.CREATE_SUCCESS;
11 | var invariantArgs: InvariantsBaseArgs = {
12 | reducerName,
13 | canBeArray: false
14 | };
15 |
16 | export default function success(
17 | config: Config,
18 | current: Map,
19 | addedRecord: any,
20 | clientGeneratedKey?: string
21 | ): Map {
22 | invariants(invariantArgs, config, current, addedRecord);
23 |
24 | var key = config.key;
25 | var addedRecordKey: string = addedRecord[key];
26 | var addedRecordKeyLens = lensProp(addedRecordKey);
27 | var currentWithoutClientGeneratedKey = dissoc(clientGeneratedKey, current);
28 |
29 | return set(
30 | addedRecordKeyLens,
31 | addedRecord,
32 | currentWithoutClientGeneratedKey
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/src/reducers/map/delete/error.test.ts:
--------------------------------------------------------------------------------
1 | import * as values from "ramda/src/values"
2 | import test from "ava";
3 |
4 | import constants from "../../../constants";
5 | import reducer from "./error";
6 |
7 | var config = {
8 | key: constants.DEFAULT_KEY,
9 | resourceName: "users"
10 | };
11 | var subject = constants.REDUCER_NAMES.DELETE_ERROR;
12 |
13 | function getCurrent() {
14 | return {
15 | 1: {
16 | id: 1,
17 | name: "Blue",
18 | deleted: true,
19 | busy: true
20 | },
21 | 2: {
22 | id: 2,
23 | name: "Red",
24 | deleted: true,
25 | busy: true
26 | }
27 | };
28 | }
29 |
30 | test(subject + "throws if given an array", function(t) {
31 | var curr = getCurrent();
32 | var record = [];
33 | function fn() {
34 | reducer(config, curr, record);
35 | }
36 |
37 | t.throws(fn, TypeError);
38 | });
39 |
40 | test(subject + "doesnt mutate", function(t) {
41 | var curr = getCurrent();
42 | var record = {
43 | id: 1
44 | };
45 |
46 | var updated = reducer(config, curr, record);
47 |
48 | t.is(curr["1"].deleted, true);
49 | t.is(curr["1"].busy, true);
50 | t.is(updated["1"].deleted, undefined);
51 | t.is(updated["1"].busy, undefined);
52 | });
53 |
54 | test(subject + "removes deleted and busy", function(t) {
55 | var curr = getCurrent();
56 | var record = {
57 | id: 1
58 | };
59 | var updated = reducer(config, curr, record);
60 |
61 | t.is(values(updated).length, 2, "doesnt remove record");
62 | t.truthy(updated["1"].deleted == null, "removes deleted");
63 | t.truthy(updated["1"].busy == null, "removes busy");
64 |
65 | t.truthy(updated["2"].deleted, "doesnt removes deleted from others");
66 | t.truthy(updated["2"].busy, "doesnt removes busy from others");
67 | });
68 |
69 | test(subject + "uses the given key", function(t) {
70 | var config = {
71 | key: "_id",
72 | resourceName: "users"
73 | };
74 | var curr = {
75 | 1: {
76 | _id: 1,
77 | deleted: true,
78 | busy: true
79 | }
80 | };
81 | var record = {
82 | _id: 1
83 | };
84 | var updated = reducer(config, curr, record);
85 |
86 | t.truthy(updated["1"].deleted == null, "removes deleted");
87 | t.truthy(updated["1"].busy == null, "removes busy");
88 | });
89 |
90 | test(subject + "it throws when record doesnt have an id", function(t) {
91 | var curr = getCurrent();
92 | var record = {
93 | name: "Green"
94 | };
95 |
96 | var f = function() {
97 | reducer(config, curr, record);
98 | };
99 | t.throws(f);
100 | });
101 |
--------------------------------------------------------------------------------
/src/reducers/map/delete/error.ts:
--------------------------------------------------------------------------------
1 | import * as omit from "ramda/src/omit"
2 | import * as merge from "ramda/src/merge"
3 |
4 | import constants from "../../../constants";
5 | import findByKey from "../../../utils/findByKey";
6 | import invariants from "../invariants";
7 | import store from "../store";
8 |
9 | import {Config, InvariantsBaseArgs, Map, ReducerName} from "../../../types";
10 |
11 | var reducerName: ReducerName = constants.REDUCER_NAMES.DELETE_ERROR;
12 | var invariantArgs: InvariantsBaseArgs = {
13 | reducerName,
14 | canBeArray: false
15 | };
16 |
17 | export default function error(
18 | config: Config,
19 | current: Map,
20 | record: any
21 | ): Map {
22 | invariants(invariantArgs, config, current, record);
23 |
24 | var key = config.key;
25 | var deleteId = record[key];
26 |
27 | // Find the record
28 | var deleteRecord = current[deleteId];
29 |
30 | if (deleteRecord == null) {
31 | return current;
32 | } else {
33 | // Remove deleted and busy
34 | deleteRecord = omit(["deleted", "busy"], deleteRecord);
35 |
36 | return merge(current, {[deleteId]: deleteRecord});
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/reducers/map/delete/start.test.ts:
--------------------------------------------------------------------------------
1 | import constants from "../../../constants";
2 | import reducer from "./start";
3 | import test from "ava";
4 |
5 | var config = {
6 | key: constants.DEFAULT_KEY,
7 | resourceName: "users"
8 | };
9 | var subject = constants.REDUCER_NAMES.DELETE_START;
10 |
11 | function getCurrent() {
12 | return {
13 | 1: {
14 | id: 1,
15 | name: "Blue"
16 | },
17 | 2: {
18 | id: 2,
19 | name: "Red"
20 | }
21 | };
22 | }
23 |
24 | function getValid() {
25 | return {
26 | id: 1,
27 | name: "Green"
28 | };
29 | }
30 |
31 | test(subject + "throws if given an array", function(t) {
32 | var curr = getCurrent();
33 | var record = [];
34 | function fn() {
35 | reducer(config, curr, record);
36 | }
37 |
38 | t.throws(fn, TypeError);
39 | });
40 |
41 | test(subject + "marks record as deleted and busy", function(t) {
42 | var curr = getCurrent();
43 | var record = getValid();
44 | var updated = reducer(config, curr, record);
45 |
46 | t.is(updated["1"].deleted, true);
47 | t.is(updated["1"].busy, true);
48 |
49 | t.truthy(updated["2"].deleted == null, "doesnt add deleted to others");
50 | t.truthy(updated["2"].busy == null, "doesnt add busy to others");
51 | });
52 |
53 | test(subject + "doesnt mutate", function(t) {
54 | var curr = getCurrent();
55 | var record = getValid();
56 | var updated = reducer(config, curr, record);
57 |
58 | t.is(updated["1"].deleted, true);
59 | t.is(curr["1"]["deleted"], undefined);
60 | });
61 |
62 | test(subject + "uses the given key", function(t) {
63 | var config = {
64 | key: "_id",
65 | resourceName: "users"
66 | };
67 | var curr = {
68 | 1: {
69 | _id: 1
70 | }
71 | };
72 | var record = {
73 | _id: 1
74 | };
75 | var updated = reducer(config, curr, record);
76 |
77 | t.truthy(updated["1"].deleted, "adds deleted");
78 | });
79 |
80 | test(subject + "it throws when record dont have an id", function(t) {
81 | var curr = getCurrent();
82 | var record = {
83 | name: "Green"
84 | };
85 |
86 | var f = function() {
87 | reducer(config, curr, record);
88 | };
89 | t.throws(f);
90 | });
91 |
--------------------------------------------------------------------------------
/src/reducers/map/delete/start.ts:
--------------------------------------------------------------------------------
1 | import {prepareRecord} from "../../common/delete/start";
2 | import invariants from "../invariants";
3 | import constants from "../../../constants";
4 | import store from "../store";
5 |
6 | import {Config, InvariantsBaseArgs, Map, ReducerName} from "../../../types";
7 |
8 | var reducerName: ReducerName = constants.REDUCER_NAMES.DELETE_START;
9 | var invariantArgs: InvariantsBaseArgs = {
10 | reducerName,
11 | canBeArray: false
12 | };
13 |
14 | export default function start(
15 | config: Config,
16 | current: Map,
17 | record: any
18 | ): Map {
19 | invariants(invariantArgs, config, current, record);
20 |
21 | var key = config.key;
22 | var deleteId = record[key];
23 | var deleteRecord = current[deleteId];
24 |
25 | if (deleteRecord == null) {
26 | return current;
27 | } else {
28 | deleteRecord = prepareRecord(deleteRecord);
29 |
30 | return store.merge(config, current, deleteRecord);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/reducers/map/delete/success.test.ts:
--------------------------------------------------------------------------------
1 | import * as values from "ramda/src/values"
2 | import test from "ava";
3 |
4 | import constants from "../../../constants";
5 | import reducer from "./success";
6 |
7 | var config = {
8 | key: constants.DEFAULT_KEY,
9 | resourceName: "users"
10 | };
11 | var subject = constants.REDUCER_NAMES.DELETE_SUCCESS;
12 |
13 | function getCurrent() {
14 | return {
15 | 1: {
16 | id: 1,
17 | name: "Blue"
18 | },
19 | 2: {
20 | id: 2,
21 | name: "Red"
22 | }
23 | };
24 | }
25 |
26 | function getValid() {
27 | return {
28 | id: 1,
29 | name: "Green"
30 | };
31 | }
32 |
33 | test(subject + "throws if given an array", function(t) {
34 | var curr = getCurrent();
35 | var record = [];
36 | function fn() {
37 | reducer(config, curr, record);
38 | }
39 |
40 | t.throws(fn, TypeError);
41 | });
42 |
43 | test(subject + "removes the record", function(t) {
44 | var curr = getCurrent();
45 | var record = getValid();
46 | var updated = reducer(config, curr, record);
47 |
48 | t.is(values(updated).length, 1, "removes the record");
49 | t.is(updated["1"], undefined);
50 | });
51 |
52 | test(subject + "doesnt mutate the original collection", function(t) {
53 | var curr = getCurrent();
54 | var record = getValid();
55 | var updated = reducer(config, curr, record);
56 |
57 | t.is(values(curr).length, 2);
58 | t.is(values(updated).length, 1);
59 | });
60 |
61 | test(subject + "uses the given key", function(t) {
62 | var config = {
63 | key: "_id",
64 | resourceName: "users"
65 | };
66 | var curr = [
67 | {
68 | _id: 1
69 | }
70 | ];
71 | var record = {
72 | _id: 1
73 | };
74 | var updated = reducer(config, curr, record);
75 |
76 | t.deepEqual(values(updated).length, 0, "removes the record");
77 | });
78 |
79 | test(subject + "it throws when record dont have an id", function(t) {
80 | var curr = getCurrent();
81 | var record = {
82 | name: "Green"
83 | };
84 |
85 | var f = function() {
86 | reducer(config, curr, record);
87 | };
88 | t.throws(f);
89 | });
90 |
--------------------------------------------------------------------------------
/src/reducers/map/delete/success.ts:
--------------------------------------------------------------------------------
1 | import * as reject from "ramda/src/reject"
2 |
3 | import invariants from "../invariants";
4 | import constants from "../../../constants";
5 |
6 | import {Config, InvariantsBaseArgs, Map, ReducerName} from "../../../types";
7 |
8 | var reducerName: ReducerName = constants.REDUCER_NAMES.DELETE_SUCCESS;
9 | var invariantArgs: InvariantsBaseArgs = {
10 | reducerName,
11 | canBeArray: false
12 | };
13 |
14 | export default function success(
15 | config: Config,
16 | current: Map,
17 | record: any
18 | ): Map {
19 | invariants(invariantArgs, config, current, record);
20 |
21 | var key = config.key;
22 | var deleteId = record[key];
23 |
24 | function predicate(existingRecord) {
25 | return deleteId == existingRecord[key];
26 | }
27 |
28 | return reject(predicate, current);
29 | }
30 |
--------------------------------------------------------------------------------
/src/reducers/map/fetch/success.test.ts:
--------------------------------------------------------------------------------
1 | import * as values from "ramda/src/values"
2 | import * as merge from "ramda/src/merge"
3 | import test from "ava";
4 |
5 | import constants from "../../../constants";
6 | import reducer from "./success";
7 |
8 | var config = {
9 | key: constants.DEFAULT_KEY,
10 | resourceName: "users"
11 | };
12 |
13 | var subject = constants.REDUCER_NAMES.FETCH_SUCCESS;
14 |
15 | function getCurrent() {
16 | return {
17 | 1: {
18 | id: 1,
19 | name: "Blue"
20 | },
21 | 2: {
22 | id: 2,
23 | name: "Red"
24 | }
25 | };
26 | }
27 |
28 | test(subject + " adds the records", function(t) {
29 | var curr = getCurrent();
30 | var more = [
31 | {
32 | id: 42,
33 | name: "Green",
34 | },
35 | ];
36 |
37 | var updated = reducer(config, curr, more, {});
38 |
39 | var expected = {
40 | 1: {
41 | id: 1,
42 | name: "Blue",
43 | },
44 | 2: {
45 | id: 2,
46 | name: "Red",
47 | },
48 | 42: {
49 | id: 42,
50 | name: "Green",
51 | },
52 | }
53 |
54 | t.deepEqual(updated, expected);
55 | });
56 |
57 | test(subject + " throws when config.key is wrong", function(t) {
58 | var curr = getCurrent();
59 | var more = [
60 | {
61 | id: 42,
62 | name: "Green",
63 | },
64 | ];
65 |
66 | var config2 = merge(config, {
67 | key: "_id",
68 | })
69 |
70 | var f = function() {
71 | reducer(config2, curr, more, {});
72 | };
73 | t.throws(f);
74 | })
75 |
76 | test(subject + " doesnt mutate the original collection", function(t) {
77 | var curr = getCurrent();
78 | var more = [
79 | {
80 | id: 3,
81 | name: "Green"
82 | }
83 | ];
84 | var updated = reducer(config, curr, more, {});
85 |
86 | t.is(values(curr).length, 2);
87 | t.is(values(updated).length, 3);
88 | });
89 |
90 | test(subject + " merges", function(t) {
91 | var curr = getCurrent();
92 | var more = [
93 | {
94 | id: 2,
95 | name: "Green"
96 | }
97 | ];
98 | var updated = reducer(config, curr, more, {});
99 |
100 | t.is(values(updated).length, 2);
101 | t.is(updated["2"].id, 2);
102 | t.is(updated["2"].name, "Green");
103 | });
104 |
105 | test(subject + " replaces", function(t) {
106 | const curr = getCurrent();
107 | const more = [
108 | {
109 | id: 2,
110 | name: "Green"
111 | }
112 | ];
113 | const updated = reducer(config, curr, more, {}, true);
114 |
115 | t.is(values(updated).length, 1);
116 | t.is(updated["2"].id, 2);
117 | t.is(updated["2"].name, "Green");
118 | });
119 |
120 | test(subject + " uses the given key", function(t) {
121 | var config = {
122 | key: "_id",
123 | resourceName: "users"
124 | };
125 | var curr = {
126 | 2: {
127 | _id: 2,
128 | name: "Blue"
129 | }
130 | };
131 | var more = [
132 | {
133 | _id: 2,
134 | name: "Green"
135 | }
136 | ];
137 |
138 | var updated = reducer(config, curr, more, {});
139 |
140 | t.is(values(updated).length, 1);
141 | });
142 |
143 | test(subject + " it throws when records dont have an id", function(t) {
144 | var curr = getCurrent();
145 | var more = [
146 | {
147 | name: "Green"
148 | }
149 | ];
150 |
151 | var f = function() {
152 | reducer(config, curr, more, {});
153 | };
154 | t.throws(f);
155 | });
156 |
157 | test(subject + " can take one record", function(t) {
158 | var curr = getCurrent();
159 | var one = {
160 | id: 3,
161 | name: "Green"
162 | };
163 | var updated = reducer(config, curr, one, {});
164 |
165 | t.is(values(updated).length, 3);
166 | });
167 |
--------------------------------------------------------------------------------
/src/reducers/map/fetch/success.ts:
--------------------------------------------------------------------------------
1 | import * as indexBy from "ramda/src/indexBy"
2 | import * as prop from "ramda/src/prop"
3 | import * as merge from "ramda/src/merge"
4 |
5 | import assertAllHaveKeys from "../../../utils/assertAllHaveKeys";
6 | import constants from "../../../constants";
7 | import invariants from "../invariants";
8 | import wrapArray from "../../../utils/wrapArray";
9 |
10 | import {Config, InvariantsBaseArgs, Map, ReducerName} from "../../../types";
11 |
12 | const reducerName: ReducerName = constants.REDUCER_NAMES.FETCH_SUCCESS;
13 | const invariantArgs: InvariantsBaseArgs = {
14 | reducerName,
15 | canBeArray: true
16 | };
17 |
18 | export default function success(
19 | config: Config,
20 | current: Map,
21 | records: any,
22 | emptyState: any,
23 | replace: boolean = false
24 | ): Map {
25 | invariants(invariantArgs, config, current, records);
26 |
27 | // wrap array
28 | records = wrapArray(records);
29 |
30 | // All given records must have a key
31 | assertAllHaveKeys(config, reducerName, records);
32 |
33 | const mergeValues = indexBy(prop(config.key), records);
34 |
35 | return merge(replace ? emptyState : current, mergeValues);
36 | }
37 |
--------------------------------------------------------------------------------
/src/reducers/map/invariants.ts:
--------------------------------------------------------------------------------
1 | import invariants from "../invariants";
2 | import store from "./store";
3 |
4 | import {Config, InvariantsBaseArgs, Map, ReducerName} from "../../types";
5 |
6 | export default function invariantsMap(
7 | invariantArgs: InvariantsBaseArgs,
8 | config: Config,
9 | current: Map,
10 | record: any
11 | ) {
12 | var extra = {
13 | assertValidStore: store.assert,
14 | config,
15 | current,
16 | record
17 | };
18 |
19 | invariants(invariantArgs, extra);
20 | }
21 |
--------------------------------------------------------------------------------
/src/reducers/map/reducersFor.test.ts:
--------------------------------------------------------------------------------
1 | import test from "ava";
2 | import * as td from "testdouble";
3 |
4 | import constants from "../../constants";
5 | import reducersFor from "./reducersFor";
6 |
7 | const current = [{}];
8 | const user = {};
9 | const error = "";
10 | const config = {
11 | key: constants.DEFAULT_KEY,
12 | resourceName: "users"
13 | };
14 | const subject = "reducersFor: ";
15 |
16 | test(subject + "calls fetchSuccess", function(t) {
17 | const fetchSuccess = td.function();
18 | const reducers = reducersFor("users", {}, {fetchSuccess});
19 |
20 | var users = [user];
21 |
22 | reducers(current, {
23 | records: users,
24 | type: "USERS_FETCH_SUCCESS"
25 | });
26 |
27 | td.verify(fetchSuccess(config, current, users, {}, undefined));
28 | t.pass();
29 | });
30 |
31 | test(subject + "calls fetchSuccess with replace", function(t) {
32 | const fetchSuccess = td.function();
33 | const reducers = reducersFor("users", {}, {fetchSuccess});
34 |
35 | var users = [user];
36 |
37 | reducers(current, {
38 | data: {replace: true},
39 | records: users,
40 | type: "USERS_FETCH_SUCCESS"
41 | });
42 |
43 | td.verify(fetchSuccess(config, current, users, {}, true));
44 | t.pass();
45 | });
46 |
47 | test(subject + "calls createStart", function(t) {
48 | const createStart = td.function();
49 | const reducers = reducersFor("users", {}, {createStart});
50 |
51 | reducers(current, {
52 | record: user,
53 | type: "USERS_CREATE_START"
54 | });
55 |
56 | td.verify(createStart(config, current, user));
57 | t.pass();
58 | });
59 |
60 | test(subject + "calls createSuccess", function(t) {
61 | const createSuccess = td.function();
62 | const reducers = reducersFor("users", {}, {createSuccess});
63 |
64 | var cid = "abc";
65 |
66 | reducers(current, {
67 | record: user,
68 | type: "USERS_CREATE_SUCCESS",
69 | cid: cid
70 | });
71 |
72 | td.verify(createSuccess(config, current, user, cid));
73 | t.pass();
74 | });
75 |
76 | test(subject + "calls createError", function(t) {
77 | const createError = td.function();
78 | const reducers = reducersFor("users", {}, {createError});
79 |
80 | reducers(current, {
81 | error: error,
82 | record: user,
83 | type: "USERS_CREATE_ERROR"
84 | });
85 |
86 | td.verify(createError(config, current, user));
87 | t.pass();
88 | });
89 |
90 | test(subject + "calls updateStart", function(t) {
91 | const updateStart = td.function();
92 | const reducers = reducersFor("users", {}, {updateStart});
93 |
94 | reducers(current, {
95 | record: user,
96 | type: "USERS_UPDATE_START"
97 | });
98 |
99 | td.verify(updateStart(config, current, user));
100 | t.pass();
101 | });
102 |
103 | test(subject + "calls updateSuccess", function(t) {
104 | const updateSuccess = td.function();
105 | const reducers = reducersFor("users", {}, {updateSuccess});
106 |
107 | reducers(current, {
108 | record: user,
109 | type: "USERS_UPDATE_SUCCESS"
110 | });
111 |
112 | td.verify(updateSuccess(config, current, user));
113 | t.pass();
114 | });
115 |
116 | test(subject + "calls updateError", function(t) {
117 | const updateError = td.function();
118 | const reducers = reducersFor("users", {}, {updateError});
119 |
120 | reducers(current, {
121 | error: error,
122 | record: user,
123 | type: "USERS_UPDATE_ERROR"
124 | });
125 |
126 | td.verify(updateError(config, current, user));
127 | t.pass();
128 | });
129 |
130 | test(subject + "calls deleteStart", function(t) {
131 | const deleteStart = td.function();
132 | const reducers = reducersFor("users", {}, {deleteStart});
133 |
134 | reducers(current, {
135 | record: user,
136 | type: "USERS_DELETE_START"
137 | });
138 |
139 | td.verify(deleteStart(config, current, user));
140 | t.pass();
141 | });
142 |
143 | test(subject + "calls deleteSuccess", function(t) {
144 | const deleteSuccess = td.function();
145 | const reducers = reducersFor("users", {}, {deleteSuccess});
146 |
147 | reducers(current, {
148 | record: user,
149 | type: "USERS_DELETE_SUCCESS"
150 | });
151 |
152 | td.verify(deleteSuccess(config, current, user));
153 | t.pass();
154 | });
155 |
156 | test(subject + "calls deleteError", function(t) {
157 | const deleteError = td.function();
158 | const reducers = reducersFor("users", {}, {deleteError});
159 |
160 | reducers(current, {
161 | error: error,
162 | record: user,
163 | type: "USERS_DELETE_ERROR"
164 | });
165 |
166 | td.verify(deleteError(config, current, user));
167 | t.pass();
168 | });
169 |
170 | test(subject + "it passes the given key", function(t) {
171 | const createStart = td.function();
172 | const reducers = reducersFor("users", {key: "_id"}, {createStart});
173 |
174 | reducers(current, {
175 | record: user,
176 | type: "USERS_CREATE_START"
177 | });
178 |
179 | var expectedConfig = {
180 | key: "_id",
181 | resourceName: "users"
182 | };
183 |
184 | td.verify(createStart(expectedConfig, current, user));
185 | t.pass();
186 | });
187 |
188 | test(subject + "it doesnt mutate the config", function(t) {
189 | const config = {};
190 | reducersFor("users", config);
191 | reducersFor("monkeys", config);
192 |
193 | t.deepEqual(config, {});
194 | });
195 |
--------------------------------------------------------------------------------
/src/reducers/map/reducersFor.ts:
--------------------------------------------------------------------------------
1 | import * as merge from "ramda/src/merge"
2 |
3 | import actionTypesFor from "../../actionTypesFor";
4 | import constants from "../../constants";
5 | import commonReducersFor from "../common/reducersFor";
6 | import createError from "./create/error";
7 | import createStart from "./create/start";
8 | import createSuccess from "./create/success";
9 | import deleteError from "./delete/error";
10 | import deleteStart from "./delete/start";
11 | import deleteSuccess from "./delete/success";
12 | import fetchSuccess from "./fetch/success";
13 | import updateError from "./update/error";
14 | import updateStart from "./update/start";
15 | import updateSuccess from "./update/success";
16 |
17 | import {Config, ReducerName} from "../../types";
18 |
19 | const baseReducers = {
20 | createError,
21 | createStart,
22 | createSuccess,
23 | deleteError,
24 | deleteStart,
25 | deleteSuccess,
26 | fetchSuccess,
27 | updateError,
28 | updateStart,
29 | updateSuccess
30 | };
31 |
32 | export default function reducersFor(resourceName: string, args = {}, deps?) {
33 | const reducers = merge(baseReducers, deps);
34 | return commonReducersFor(resourceName, args, {}, reducers);
35 | }
36 |
--------------------------------------------------------------------------------
/src/reducers/map/store.ts:
--------------------------------------------------------------------------------
1 | import {Config, Map} from "../../types";
2 | import assert from "./store/assert";
3 | import merge from "./store/merge";
4 | import remove from "./store/remove";
5 |
6 | export default {
7 | assert,
8 | merge,
9 | remove
10 | };
11 |
--------------------------------------------------------------------------------
/src/reducers/map/store/assert.ts:
--------------------------------------------------------------------------------
1 | import * as is from "ramda/src/is"
2 |
3 | import {Map} from "../../../types";
4 |
5 | export default function assertValidStore(
6 | scope: string,
7 | current: Map
8 | ): void {
9 | if (!is(Object, current))
10 | throw new Error(scope + ": Expected current to be an object");
11 | }
12 |
--------------------------------------------------------------------------------
/src/reducers/map/store/merge.ts:
--------------------------------------------------------------------------------
1 | import * as merge from "ramda/src/merge"
2 |
3 | import {Config, Map} from "../../../types";
4 |
5 | /*
6 | Adds or replace one record
7 | */
8 | export default function replace(
9 | config: Config,
10 | current: Map,
11 | record: any
12 | ): Map {
13 | var key = config.key;
14 | var recordKey = record[key];
15 |
16 | return merge(current, {[recordKey]: record});
17 | }
18 |
--------------------------------------------------------------------------------
/src/reducers/map/store/remove.ts:
--------------------------------------------------------------------------------
1 | import * as omit from "ramda/src/omit"
2 |
3 | import {Config, Map} from "../../../types";
4 |
5 | export default function remove(
6 | config: Config,
7 | current: Map,
8 | record: any
9 | ): Map {
10 | var key = config.key;
11 | var recordKey = record[key];
12 |
13 | return omit([recordKey], current);
14 | }
15 |
--------------------------------------------------------------------------------
/src/reducers/map/update/error.test.ts:
--------------------------------------------------------------------------------
1 | import * as values from "ramda/src/values"
2 | import test from "ava";
3 |
4 | import constants from "../../../constants";
5 | import reducer from "./error";
6 |
7 | var config = {
8 | key: constants.DEFAULT_KEY,
9 | resourceName: "users"
10 | };
11 | var subject = constants.REDUCER_NAMES.UPDATE_ERROR;
12 |
13 | function getCurrent() {
14 | return {
15 | 1: {
16 | id: 1,
17 | name: "Blue",
18 | busy: true,
19 | pendingUpdate: true
20 | },
21 | 2: {
22 | id: 2,
23 | name: "Red",
24 | busy: true,
25 | pendingUpdate: true
26 | }
27 | };
28 | }
29 |
30 | function getValid() {
31 | return {
32 | id: 2,
33 | name: "Green"
34 | };
35 | }
36 |
37 | test(subject + "throws if given an array", function(t) {
38 | var curr = getCurrent();
39 | var record = [];
40 | function fn() {
41 | reducer(config, curr, record);
42 | }
43 |
44 | t.throws(fn, TypeError);
45 | });
46 |
47 | test(subject + "doesnt add record if not there", function(t) {
48 | var curr = getCurrent();
49 | var record = {
50 | id: 3,
51 | name: "Green"
52 | };
53 | var updated = reducer(config, curr, record);
54 |
55 | t.is(values(updated).length, 2);
56 | });
57 |
58 | test(subject + "removes busy", function(t) {
59 | var curr = getCurrent();
60 | var record = getValid();
61 | var updated = reducer(config, curr, record);
62 |
63 | t.truthy(updated["1"].busy, "doesnt remove on others");
64 | t.truthy(updated["2"].busy == null, "removes busy");
65 | });
66 |
67 | test(subject + "doesnt mutate the original collection", function(t) {
68 | var curr = getCurrent();
69 | var record = getValid();
70 | var updated = reducer(config, curr, record);
71 |
72 | t.is(curr["2"].busy, true);
73 | t.is(updated["2"].busy, undefined);
74 | });
75 |
76 | test(subject + "doesnt remove pendingUpdate", function(t) {
77 | var curr = getCurrent();
78 | var record = getValid();
79 | var updated = reducer(config, curr, record);
80 |
81 | t.truthy(updated["2"].pendingUpdate);
82 | });
83 |
84 | test(subject + "uses the given key", function(t) {
85 | var config = {
86 | key: "_id",
87 | resourceName: "users"
88 | };
89 | var curr = {
90 | 2: {
91 | _id: 2,
92 | name: "Blue",
93 | busy: true,
94 | unsaved: true
95 | }
96 | };
97 | var record = {
98 | _id: 2
99 | };
100 | var updated = reducer(config, curr, record);
101 |
102 | t.truthy(updated["2"].busy == null, "removes busy");
103 | });
104 |
105 | test(subject + "it throws when record dont have an id", function(t) {
106 | var curr = getCurrent();
107 | var record = {
108 | name: "Green"
109 | };
110 |
111 | var f = function() {
112 | reducer(config, curr, record);
113 | };
114 |
115 | t.throws(f);
116 | });
117 |
--------------------------------------------------------------------------------
/src/reducers/map/update/error.ts:
--------------------------------------------------------------------------------
1 | import {prepareRecord} from "../../common/update/error";
2 | import constants from "../../../constants";
3 | import invariants from "../invariants";
4 | import store from "../store";
5 |
6 | import {Config, InvariantsBaseArgs, Map, ReducerName} from "../../../types";
7 |
8 | var reducerName: ReducerName = constants.REDUCER_NAMES.UPDATE_ERROR;
9 | var invariantArgs: InvariantsBaseArgs = {
10 | reducerName,
11 | canBeArray: false
12 | };
13 |
14 | export default function error(
15 | config: Config,
16 | current: Map,
17 | record: any
18 | ): Map {
19 | invariants(invariantArgs, config, current, record);
20 |
21 | // We don"t want to rollback
22 | var key = config.key;
23 | var updatedId = record[key];
24 | var updatedRecord = current[updatedId];
25 |
26 | if (updatedRecord == null) return current;
27 |
28 | updatedRecord = prepareRecord(updatedRecord);
29 |
30 | return store.merge(config, current, updatedRecord);
31 | }
32 |
--------------------------------------------------------------------------------
/src/reducers/map/update/start.test.ts:
--------------------------------------------------------------------------------
1 | import * as values from "ramda/src/values"
2 | import test from "ava";
3 |
4 | import constants from "../../../constants";
5 | import reducer from "./start";
6 |
7 | var config = {
8 | key: constants.DEFAULT_KEY,
9 | resourceName: "users"
10 | };
11 | var subject = constants.REDUCER_NAMES.UPDATE_START;
12 |
13 | function getCurrent() {
14 | return {
15 | 1: {
16 | id: 1,
17 | name: "Blue"
18 | },
19 | 2: {
20 | id: 2,
21 | name: "Red"
22 | }
23 | };
24 | }
25 |
26 | function getValid() {
27 | return {
28 | id: 2,
29 | name: "Green"
30 | };
31 | }
32 |
33 | test(subject + "throws if given an array", function(t) {
34 | var curr = getCurrent();
35 | var record = [];
36 | function fn() {
37 | reducer(config, curr, record);
38 | }
39 |
40 | t.throws(fn, TypeError);
41 | });
42 |
43 | test(subject + "adds the record if not there", function(t) {
44 | var curr = getCurrent();
45 | var record = {
46 | id: 3,
47 | name: "Green"
48 | };
49 | var updated = reducer(config, curr, record);
50 |
51 | t.is(values(updated).length, 3);
52 | });
53 |
54 | test(subject + "doesnt mutate the original", function(t) {
55 | var curr = getCurrent();
56 | var record = {
57 | id: 3,
58 | name: "Green"
59 | };
60 | var updated = reducer(config, curr, record);
61 |
62 | t.is(values(curr).length, 2);
63 | t.is(values(updated).length, 3);
64 | });
65 |
66 | test(subject + "updates existing", function(t) {
67 | var curr = getCurrent();
68 | var record = getValid();
69 | var updated = reducer(config, curr, record);
70 |
71 | t.is(values(updated).length, 2);
72 | t.is(updated["2"].id, 2);
73 | t.is(updated["2"].name, "Green");
74 | });
75 |
76 | test(subject + "uses the given key", function(t) {
77 | var config = {
78 | key: "_id",
79 | resourceName: "users"
80 | };
81 | var curr = {
82 | 2: {
83 | _id: 2,
84 | name: "Blue"
85 | }
86 | };
87 | var record = {
88 | _id: 2,
89 | name: "Green"
90 | };
91 | var updated = reducer(config, curr, record);
92 |
93 | t.is(values(updated).length, 1);
94 | });
95 |
96 | test(subject + "it throws when record dont have an id", function(t) {
97 | var curr = getCurrent();
98 | var record = {
99 | name: "Green"
100 | };
101 |
102 | var f = function() {
103 | reducer(config, curr, record);
104 | };
105 | t.throws(f);
106 | });
107 |
108 | test(subject + "adds busy and pendingUpdate", function(t) {
109 | var curr = getCurrent();
110 | var record = getValid();
111 | var updated = reducer(config, curr, record);
112 |
113 | t.deepEqual(updated["2"].name, "Green");
114 | t.truthy(updated["2"].busy, "adds busy");
115 | t.truthy(updated["2"].pendingUpdate, "adds pendingUpdate");
116 | });
117 |
--------------------------------------------------------------------------------
/src/reducers/map/update/start.ts:
--------------------------------------------------------------------------------
1 | import {prepareRecord} from "../../common/update/start";
2 | import constants from "../../../constants";
3 | import invariants from "../invariants";
4 | import store from "../store";
5 |
6 | import {Config, InvariantsBaseArgs, Map, ReducerName} from "../../../types";
7 |
8 | var reducerName: ReducerName = constants.REDUCER_NAMES.UPDATE_START;
9 | var invariantArgs: InvariantsBaseArgs = {
10 | reducerName,
11 | canBeArray: false
12 | };
13 |
14 | export default function start(
15 | config: Config,
16 | current: Map,
17 | record: any
18 | ): Map {
19 | invariants(invariantArgs, config, current, record);
20 |
21 | // mark record as unsaved and busy
22 | var newRecord = prepareRecord(record);
23 |
24 | // replace record
25 | return store.merge(config, current, newRecord);
26 | }
27 |
--------------------------------------------------------------------------------
/src/reducers/map/update/success.test.ts:
--------------------------------------------------------------------------------
1 | import * as values from "ramda/src/values"
2 |
3 | import constants from "../../../constants";
4 | import reducer from "./success";
5 | import test from "ava";
6 |
7 | var config = {
8 | key: constants.DEFAULT_KEY,
9 | resourceName: "users"
10 | };
11 | var subject = constants.REDUCER_NAMES.UPDATE_SUCCESS;
12 |
13 | function getCurrent() {
14 | return {
15 | 1: {
16 | id: 1,
17 | name: "Blue",
18 | unsaved: true,
19 | busy: true
20 | },
21 | 2: {
22 | id: 2,
23 | name: "Red",
24 | unsaved: true,
25 | busy: true
26 | }
27 | };
28 | }
29 |
30 | function getValid() {
31 | return {
32 | id: 2,
33 | name: "Green"
34 | };
35 | }
36 |
37 | test(subject + "throws if given an array", function(t) {
38 | var curr = getCurrent();
39 | var record = [];
40 | function fn() {
41 | reducer(config, curr, record);
42 | }
43 |
44 | t.throws(fn, TypeError);
45 | });
46 |
47 | test(subject + "adds the record if not there", function(t) {
48 | var curr = getCurrent();
49 | var record = {
50 | id: 3,
51 | name: "Green"
52 | };
53 | var updated = reducer(config, curr, record);
54 |
55 | t.is(values(updated).length, 3);
56 | });
57 |
58 | test(subject + "doesnt mutate the original collection", function(t) {
59 | var curr = getCurrent();
60 | var record = {
61 | id: 3,
62 | name: "Green"
63 | };
64 | var updated = reducer(config, curr, record);
65 |
66 | t.is(values(curr).length, 2);
67 | t.is(values(updated).length, 3);
68 | });
69 |
70 | test(subject + "updates existing", function(t) {
71 | var curr = getCurrent();
72 | var record = getValid();
73 | var updated = reducer(config, curr, record);
74 |
75 | t.is(values(updated).length, 2);
76 | t.is(updated["2"].id, 2);
77 | t.is(updated["2"].name, "Green");
78 | });
79 |
80 | test(subject + "uses the given key", function(t) {
81 | var config = {
82 | key: "_id",
83 | resourceName: "users"
84 | };
85 | var curr = {
86 | 2: {
87 | _id: 2,
88 | name: "Blue"
89 | }
90 | };
91 | var record = {
92 | _id: 2,
93 | name: "Green"
94 | };
95 | var updated = reducer(config, curr, record);
96 |
97 | t.is(values(updated).length, 1);
98 | });
99 |
100 | test(subject + "it throws when record dont have an id", function(t) {
101 | var curr = getCurrent();
102 | var record = {
103 | name: "Green"
104 | };
105 |
106 | var f = function() {
107 | reducer(config, curr, record);
108 | };
109 | t.throws(f);
110 | });
111 |
112 | test(subject + "removes busy and pendingUpdate", function(t) {
113 | var curr = {
114 | 2: {
115 | id: 2,
116 | name: "Green",
117 | pendingUpdate: true,
118 | busy: true
119 | }
120 | };
121 | var record = getValid();
122 | var updated = reducer(config, curr, record);
123 |
124 | t.deepEqual(values(updated).length, 1);
125 | t.truthy(updated["2"].busy == null, "removes busy");
126 | t.truthy(updated["2"].pendingUpdate == null, "removes pendingUpdate");
127 | });
128 |
--------------------------------------------------------------------------------
/src/reducers/map/update/success.ts:
--------------------------------------------------------------------------------
1 | import constants from "../../../constants";
2 | import invariants from "../invariants";
3 | import store from "../store";
4 |
5 | import {Config, InvariantsBaseArgs, Map, ReducerName} from "../../../types";
6 |
7 | var reducerName: ReducerName = constants.REDUCER_NAMES.UPDATE_SUCCESS;
8 | var invariantArgs: InvariantsBaseArgs = {
9 | reducerName,
10 | canBeArray: false
11 | };
12 |
13 | export default function success(
14 | config: Config,
15 | current: Map,
16 | record: any
17 | ): Map {
18 | invariants(invariantArgs, config, current, record);
19 |
20 | return store.merge(config, current, record);
21 | }
22 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface Config {
2 | key?: string,
3 | resourceName: string
4 | }
5 |
6 | export interface InvariantsBaseArgs {
7 | reducerName: ReducerName,
8 | canBeArray: boolean
9 | }
10 |
11 | export interface InvariantsExtraArgs {
12 | assertValidStore: (scope: string, current: any) => void,
13 | config: Config,
14 | current: any,
15 | record: any
16 | }
17 |
18 | export interface Map {
19 | [key: string]: T
20 | }
21 |
22 | export type ReducerName =
23 | | "createError"
24 | | "createSuccess"
25 | | "createStart"
26 | | "deleteError"
27 | | "deleteSuccess"
28 | | "deleteStart"
29 | | "fetchSuccess"
30 | | "fetchError"
31 | | "updateError"
32 | | "updateSuccess"
33 | | "updateStart";
34 |
35 | export interface StoreList {
36 | remove: (config: Config, current: Array, record: any) => Array
37 | }
38 |
39 | export interface StoreMap {
40 | remove: (config: Config, current: Map, record: any) => Map
41 | }
42 |
43 | export interface Record {
44 | id: string | number,
45 | _cid?: string | number,
46 | busy?: boolean,
47 | deleted?: boolean,
48 | pendingCreate?: boolean,
49 | pendingUpdate?: boolean
50 | }
51 |
--------------------------------------------------------------------------------
/src/utils/assertAllHaveKeys.ts:
--------------------------------------------------------------------------------
1 | import * as has from "ramda/src/has"
2 | import * as all from "ramda/src/all"
3 |
4 | export default function(config, reducerName, records) {
5 | // All given records must have a key
6 | var haskey = has(config.key);
7 | var allKeys = all(haskey, records);
8 |
9 | if (!allKeys) {
10 | throw new Error(
11 | reducerName +
12 | ": Expected all records to have a value for the store's key `" +
13 | config.key +
14 | "`"
15 | );
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/utils/assertNotArray.ts:
--------------------------------------------------------------------------------
1 | import * as is from "ramda/src/is"
2 |
3 | import makeScope from "../utils/makeScope";
4 |
5 | import {Config, ReducerName} from "../types";
6 |
7 | export default function(config: Config, reducerName: ReducerName, record: any) {
8 | var scope = makeScope(config, reducerName);
9 | var isArray = is(Array, record);
10 |
11 | if (isArray)
12 | throw new TypeError(scope + ": Expected record not to be an array");
13 | }
14 |
--------------------------------------------------------------------------------
/src/utils/findByKey.ts:
--------------------------------------------------------------------------------
1 | import * as find from "ramda/src/find"
2 |
3 | export default function findByKey(collection, key, id) {
4 | function predicate(record) {
5 | return record[key] === id;
6 | }
7 |
8 | return find(predicate, collection);
9 | }
10 |
--------------------------------------------------------------------------------
/src/utils/makeScope.ts:
--------------------------------------------------------------------------------
1 | import {Config, ReducerName} from "../types";
2 |
3 | export default function makeScope(
4 | config: Config,
5 | reducerName: ReducerName
6 | ): string {
7 | return config.resourceName + "." + reducerName;
8 | }
9 |
--------------------------------------------------------------------------------
/src/utils/wrapArray.ts:
--------------------------------------------------------------------------------
1 | import * as is from "ramda/src/is"
2 |
3 | export default function wrapArray(recordOrRecords) {
4 | var isArray = is(Array, recordOrRecords);
5 | return isArray ? recordOrRecords : [recordOrRecords];
6 | }
7 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["es2015"],
4 | "module": "commonjs",
5 | "moduleResolution": "node",
6 | "declaration": true,
7 | "outDir": "./dist",
8 | "target": "es5",
9 | "allowSyntheticDefaultImports": true
10 | },
11 | "exclude": [
12 | "dist",
13 | "node_modules",
14 | "example"
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | /*
3 | * Possible values:
4 | * - the name of a built-in config
5 | * - the name of an NPM module which has a "main" file that exports a config object
6 | * - a relative path to a JSON file
7 | */
8 | "extends": "tslint:recommended",
9 | "rules": {
10 | /*
11 | * Any rules specified here will override those from the base config we are extending.
12 | */
13 | "curly": false,
14 | "no-var-keyword": false,
15 | "only-arrow-functions": false,
16 | "ordered-imports": false,
17 | "trailing-comma": false
18 | },
19 | "jsRules": {
20 | /*
21 | * Any rules specified here will override those from the base config we are extending.
22 | */
23 | "curly": true
24 | },
25 | "rulesDirectory": [
26 | /*
27 | * A list of relative or absolute paths to directories that contain custom rules.
28 | * See the Custom Rules documentation below for more details.
29 | */
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------