Skip to content

Commit 524e631

Browse files
authored
v0.3.1
* fix(query): multiple `where` or `orderBy` parameters - compojoom * fix(orderedReducer): subcollection not updating `state.ordered` - #46 * fix(orderedReducer): multiple subcollections on same parent collection correctly merges doc data - #34 * fix(dataReducer): `setListeners` clears out the subcollection of a document - #49 * feat(orderedReducer): `mergeOrdered`, `mergeOrderedDocUpdates`, and `mergeOrderedCollectionUpdates` config options added to allow for enabling/disabling merging of ordered data within `orderedReducer` * feat(README): FAQ section added to clarify common questions - #47 * feat(README): Config Options section added to explain config options and their default values
2 parents 65ea61e + 778cc69 commit 524e631

12 files changed

+220
-29
lines changed

README.md

+80-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Most likely, you'll want react bindings, for that you will need [react-redux-fir
3131
npm install --save react-redux-firebase
3232
```
3333

34-
[react-redux-firebase](https://github.com/prescottprue/react-redux-firebase) provides [`withFirestore`](http://docs.react-redux-firebase.com/history/v2.0.0/docs/api/withFirestore.html) and [`firestoreConnect`](http://docs.react-redux-firebase.com/history/v2.0.0/docs/api/withFirestore.html) higher order components, which handle automatically calling `redux-firestore` internally based on component's lifecycle (i.e. mounting/un-mounting)
34+
[react-redux-firebase](https://github.com/prescottprue/react-redux-firebase) provides [`withFirestore`](http://react-redux-firebase.com/docs/api/withFirestore.html) and [`firestoreConnect`](http://react-redux-firebase.com/docs/api/firestoreConnect.html) higher order components, which handle automatically calling `redux-firestore` internally based on component's lifecycle (i.e. mounting/un-mounting)
3535

3636
## Use
3737

@@ -396,6 +396,61 @@ const enhance = compose(
396396
export default enhance(SomeComponent)
397397
```
398398

399+
## Config Options
400+
401+
#### enableLogging
402+
Default: `false`
403+
404+
Whether or not to enable Firebase client logging.
405+
406+
#### logListenerError
407+
Default: `true`
408+
409+
Whether or not to use `console.error` to log listener error objects. Errors from listeners are helpful to developers on multiple occasions including when index needs to be added.
410+
411+
#### enhancerNamespace
412+
Default: `'firestore'`
413+
414+
Namespace under which enhancer places internal instance on redux store (i.e. `store.firestore`).
415+
416+
#### allowMultipleListeners
417+
Default: `false`
418+
419+
Whether or not to allow multiple listeners to be attached for the same query. If a function is passed the arguments it receives are `listenerToAttach`, `currentListeners`, and the function should return a boolean.
420+
421+
#### preserveOnDelete
422+
Default: `null`
423+
424+
Values to preserve from state when DELETE_SUCCESS action is dispatched. Note that this will not prevent the LISTENER_RESPONSE action from removing items from state.ordered if you have a listener attached.
425+
426+
#### preserveOnListenerError
427+
Default: `null`
428+
429+
Values to preserve from state when LISTENER_ERROR action is dispatched.
430+
431+
#### onAttemptCollectionDelete
432+
Default: `null`
433+
434+
Arguments:`(queryOption, dispatch, firebase)`
435+
436+
Function run when attempting to delete a collection. If not provided (default) delete promise will be rejected with "Only documents can be deleted" unless. This is due to the fact that Collections can not be deleted from a client, it should instead be handled within a cloud function (which can be called by providing a promise to `onAttemptCollectionDelete` that calls the cloud function).
437+
438+
#### mergeOrdered
439+
Default: `true`
440+
441+
Whether or not to merge data within `orderedReducer`.
442+
443+
#### mergeOrderedDocUpdate
444+
Default: `true`
445+
446+
Whether or not to merge data from document listener updates within `orderedReducer`.
447+
448+
449+
#### mergeOrderedCollectionUpdates
450+
Default: `true`
451+
452+
Whether or not to merge data from collection listener updates within `orderedReducer`.
453+
399454
<!-- #### Middleware
400455
401456
`redux-firestore`'s enhancer offers a new middleware setup that was not offered in `react-redux-firebase` (but will eventually make it `redux-firebase`)
@@ -443,6 +498,30 @@ Note: In an effort to keep things simple, the wording from this explanation was
443498
## Applications Using This
444499
* [fireadmin.io](http://fireadmin.io) - Firebase Instance Management Tool (source [available here](https://github.com/prescottprue/fireadmin))
445500

501+
## FAQ
502+
1. How do I update a document within a subcollection?
503+
504+
Provide `subcollections` config the same way you do while querying:
505+
506+
```js
507+
props.firestore.update(
508+
{
509+
collection: 'cities',
510+
doc: 'SF',
511+
subcollections: [{ collection: 'counties', doc: 'San Mateo' }],
512+
},
513+
{ some: 'changes' }
514+
);
515+
```
516+
517+
1. How do I get auth state in redux?
518+
519+
You will most likely want to use [`react-redux-firebase`](https://github.com/prescottprue/react-redux-firebase) or another redux/firebase connector. For more information please visit the [complementary package section](#complementary-package).
520+
521+
1. Are there Higher Order Components for use with React?
522+
523+
[`react-redux-firebase`](https://github.com/prescottprue/react-redux-firebase) contains `firebaseConnect`, `firestoreConnect`, `withFirebase` and `withFirestore` HOCs. For more information please visit the [complementary package section](#complementary-package).
524+
446525
## Roadmap
447526

448527
* Automatic support for documents that have a parameter and a subcollection with the same name (currently requires `storeAs`)

package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "redux-firestore",
3-
"version": "0.3.0",
3+
"version": "0.3.1",
44
"description": "Redux bindings for Firestore.",
55
"main": "lib/index.js",
66
"module": "es/index.js",

src/actions/firestore.js

+28-4
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ export function set(firebase, dispatch, queryOption, ...args) {
7171
export function get(firebase, dispatch, queryOption) {
7272
const meta = getQueryConfig(queryOption);
7373
// Wrap get call in dispatch calls
74+
const {
75+
mergeOrdered,
76+
mergeOrderedDocUpdates,
77+
mergeOrderedCollectionUpdates,
78+
} =
79+
firebase._.config || {};
7480
return wrapInDispatch(dispatch, {
7581
ref: firestoreRef(firebase, dispatch, meta),
7682
method: 'get',
@@ -83,6 +89,10 @@ export function get(firebase, dispatch, queryOption) {
8389
data: dataByIdSnapshot(snap),
8490
ordered: orderedFromSnap(snap),
8591
}),
92+
merge: {
93+
docs: mergeOrdered && mergeOrderedDocUpdates,
94+
collections: mergeOrdered && mergeOrderedCollectionUpdates,
95+
},
8696
},
8797
actionTypes.GET_FAILURE,
8898
],
@@ -167,6 +177,12 @@ export function deleteRef(firebase, dispatch, queryOption) {
167177
*/
168178
export function setListener(firebase, dispatch, queryOpts, successCb, errorCb) {
169179
const meta = getQueryConfig(queryOpts);
180+
const {
181+
mergeOrdered,
182+
mergeOrderedDocUpdates,
183+
mergeOrderedCollectionUpdates,
184+
} =
185+
firebase._.config || {};
170186
// Create listener
171187
const unsubscribe = firestoreRef(firebase, dispatch, meta).onSnapshot(
172188
docData => {
@@ -177,21 +193,29 @@ export function setListener(firebase, dispatch, queryOpts, successCb, errorCb) {
177193
data: dataByIdSnapshot(docData),
178194
ordered: orderedFromSnap(docData),
179195
},
196+
merge: {
197+
docs: mergeOrdered && mergeOrderedDocUpdates,
198+
collections: mergeOrdered && mergeOrderedCollectionUpdates,
199+
},
180200
});
181201
// Invoke success callback if it exists
182202
if (successCb) successCb(docData);
183203
},
184204
err => {
185205
// TODO: Look into whether listener is automatically removed in all cases
186-
// TODO: Provide a setting that allows for silencing of console error
187-
const { config } = firebase._;
188206
// Log error handling the case of it not existing
189-
if (config.logListenerError) invoke(console, 'error', err);
207+
const { logListenerError, preserveOnListenerError } =
208+
firebase._.config || {};
209+
if (logListenerError) invoke(console, 'error', err);
190210
dispatch({
191211
type: actionTypes.LISTENER_ERROR,
192212
meta,
193213
payload: err,
194-
preserve: config.preserveOnListenerError,
214+
merge: {
215+
docs: mergeOrdered && mergeOrderedDocUpdates,
216+
collections: mergeOrdered && mergeOrderedCollectionUpdates,
217+
},
218+
preserve: preserveOnListenerError,
195219
});
196220
// Invoke error callback if it exists
197221
if (errorCb) errorCb(err);

src/constants.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export const actionTypes = {
9191
* state.ordered if you have a listener attached.
9292
* @property {Object} preserveOnListenerError - `null` Values to
9393
* preserve from state when LISTENER_ERROR action is dispatched.
94-
* @property {Boolean} enhancerNamespace - `'firestore'` Namespace underwhich
94+
* @property {Boolean} enhancerNamespace - `'firestore'` Namespace under which
9595
* enhancer places internal instance on redux store (i.e. store.firestore).
9696
* @property {Boolean|Function} allowMultipleListeners - `null` Whether or not
9797
* to allow multiple listeners to be attached for the same query. If a function
@@ -115,6 +115,9 @@ export const defaultConfig = {
115115
preserveOnDelete: null,
116116
preserveOnListenerError: null,
117117
onAttemptCollectionDelete: null,
118+
mergeOrdered: true,
119+
mergeOrderedDocUpdates: true,
120+
mergeOrderedCollectionUpdates: true,
118121
};
119122

120123
export default {

src/reducers/dataReducer.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { get } from 'lodash';
2-
import { setWith, assign } from 'lodash/fp';
2+
import { setWith, merge } from 'lodash/fp';
33
import { actionTypes } from '../constants';
44
import { pathFromMeta, preserveValuesFromState } from '../utils/reducers';
55

@@ -50,7 +50,7 @@ export default function dataReducer(state = {}, action) {
5050
return setWith(Object, pathFromMeta(meta), data, state);
5151
}
5252
// Otherwise merge with existing data
53-
const mergedData = assign(previousData, data);
53+
const mergedData = merge(previousData, data);
5454
// Set data to state (with merge) immutabily (lodash/fp's setWith creates copy)
5555
return setWith(Object, pathFromMeta(meta), mergedData, state);
5656
case DELETE_SUCCESS:

src/reducers/orderedReducer.js

+19-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { first } from 'lodash';
1+
import { first, size, get, unionBy } from 'lodash';
2+
import { merge as mergeObjects } from 'lodash/fp';
23
import { actionTypes } from '../constants';
34
import { updateItemInArray, preserveValuesFromState } from '../utils/reducers';
45

@@ -20,15 +21,14 @@ function updateDocInOrdered(state, action) {
2021
const itemToAdd = first(action.payload.ordered);
2122
const subcollection = first(action.meta.subcollections);
2223
const storeUnderKey = action.meta.storeAs || action.meta.collection;
23-
// TODO: Make this recursive so that is supports multiple subcollections
2424
return {
2525
...state,
2626
[storeUnderKey]: updateItemInArray(
2727
state[storeUnderKey] || [],
2828
action.meta.doc,
2929
item =>
30-
Object.assign(
31-
{},
30+
// Use merge to preserve existing subcollections
31+
mergeObjects(
3232
item,
3333
subcollection
3434
? { [subcollection.collection]: action.payload.ordered }
@@ -65,13 +65,25 @@ export default function orderedReducer(state = {}, action) {
6565
if (!action.payload || !action.payload.ordered) {
6666
return state;
6767
}
68-
// TODO: Support merging
69-
if (action.meta.doc) {
68+
const { meta, merge = { doc: true, collection: true } } = action;
69+
const parentPath = meta.storeAs || meta.collection;
70+
// Handle doc update (update item in array instead of whole array)
71+
if (meta.doc && merge.doc && size(get(state, parentPath))) {
72+
// Merge if data already exists
73+
// Merge with existing ordered array if collection merge enabled
7074
return updateDocInOrdered(state, action);
7175
}
76+
const parentData = get(state, parentPath);
77+
// Merge with existing ordered array if collection merge enabled
78+
if (merge.collection && size(parentData)) {
79+
return {
80+
...state,
81+
[parentPath]: unionBy(parentData, action.payload.ordered, 'id'),
82+
};
83+
}
7284
return {
7385
...state,
74-
[action.meta.storeAs || action.meta.collection]: action.payload.ordered,
86+
[parentPath]: action.payload.ordered,
7587
};
7688
case CLEAR_DATA:
7789
// support keeping data when logging out - #125

src/utils/actions.js

+4
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ export function wrapInDispatch(
4747
if (successIsObject && successType.preserve) {
4848
actionObj.preserve = successType.preserve;
4949
}
50+
// Attach merge to action if it is passed
51+
if (successIsObject && successType.merge) {
52+
actionObj.merge = successType.merge;
53+
}
5054
dispatch(actionObj);
5155
return result;
5256
})

src/utils/query.js

+36-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
import { isObject, isString, isArray, size, trim, forEach, has } from 'lodash';
1+
import {
2+
isObject,
3+
isString,
4+
isArray,
5+
size,
6+
trim,
7+
forEach,
8+
has,
9+
isFunction,
10+
} from 'lodash';
211
import { actionTypes } from '../constants';
312

413
/**
@@ -15,7 +24,8 @@ function addWhereToRef(ref, where) {
1524
if (isString(where[0])) {
1625
return where.length > 1 ? ref.where(...where) : ref.where(where[0]);
1726
}
18-
return where.reduce((acc, whereArgs) => addWhereToRef(ref, whereArgs), ref);
27+
28+
return where.reduce((acc, whereArgs) => addWhereToRef(acc, whereArgs), ref);
1929
}
2030

2131
/**
@@ -37,16 +47,33 @@ function addOrderByToRef(ref, orderBy) {
3747
return ref.orderBy(...orderBy);
3848
}
3949
return orderBy.reduce(
40-
(acc, orderByArgs) => addOrderByToRef(ref, orderByArgs),
50+
(acc, orderByArgs) => addOrderByToRef(acc, orderByArgs),
4151
ref,
4252
);
4353
}
4454

45-
/* eslint-disable no-param-reassign */
55+
/**
56+
* Call methods on ref object for provided subcollection list (from queryConfig
57+
* object)
58+
* @param {firebase.firestore.CollectionReference} ref - reference on which
59+
* to call methods to apply queryConfig
60+
* @param {Array} subcollectionList - List of subcollection settings from
61+
* queryConfig object
62+
* @return {firebase.firestore.Query} Query object referencing path within
63+
* firestore
64+
*/
4665
function handleSubcollections(ref, subcollectionList) {
4766
if (subcollectionList) {
4867
forEach(subcollectionList, subcollection => {
68+
/* eslint-disable no-param-reassign */
4969
if (subcollection.collection) {
70+
if (!isFunction(ref.collection)) {
71+
throw new Error(
72+
`Collection can only be run on a document. Check that query config for subcollection: "${
73+
subcollection.collection
74+
}" contains a doc parameter.`,
75+
);
76+
}
5077
ref = ref.collection(subcollection.collection);
5178
}
5279
if (subcollection.doc) ref = ref.doc(subcollection.doc);
@@ -61,11 +88,13 @@ function handleSubcollections(ref, subcollectionList) {
6188
}
6289
if (subcollection.endAt) ref = ref.endAt(subcollection.endAt);
6390
if (subcollection.endBefore) ref = ref.endBefore(subcollection.endBefore);
64-
handleSubcollections(subcollection.subcollections);
91+
/* eslint-enable */
92+
93+
handleSubcollections(ref, subcollection.subcollections);
6594
});
6695
}
96+
return ref;
6797
}
68-
/* eslint-enable */
6998

7099
/**
71100
* Create a Cloud Firestore reference for a collection or document
@@ -96,7 +125,7 @@ export function firestoreRef(firebase, dispatch, meta) {
96125
let ref = firebase.firestore().collection(collection);
97126
// TODO: Compare other ways of building ref
98127
if (doc) ref = ref.doc(doc);
99-
handleSubcollections(ref, subcollections);
128+
ref = handleSubcollections(ref, subcollections);
100129
if (where) ref = addWhereToRef(ref, where);
101130
if (orderBy) ref = addOrderByToRef(ref, orderBy);
102131
if (limit) ref = ref.limit(limit);

0 commit comments

Comments
 (0)