Skip to content

Commit b7d4a4b

Browse files
authored
v0.5.1
* fix(orderedReducer): remove `storeAs` from `updateItemInArray` - #91 * feat(actions): `runTransaction` action added - #76 * feat(core): Firebase's Firestore internals (from `firebase.firestore()`) exposed for use of methods such as `batch` - #76 * feat(tests): unit tests added for `oneListenerPerPath` option - #77
2 parents a2764a7 + c2a789d commit b7d4a4b

11 files changed

+254
-21
lines changed

README.md

+54-1
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,6 @@ For more information [on using recompose visit the docs](https://github.com/acdl
131131
import React, { Component } from 'react'
132132
import PropTypes from 'prop-types'
133133
import { connect } from 'react-redux'
134-
import { isEqual } from 'lodash'
135134
import { watchEvents, unWatchEvents } from './actions/query'
136135
import { getEventsFromInput, createCallable } from './utils'
137136

@@ -164,6 +163,60 @@ export default connect((state) => ({
164163
todos: state.firestore.ordered.todos
165164
}))(Todos)
166165
```
166+
### API
167+
The `store.firestore` instance created by the `reduxFirestore` enhancer extends [Firebase's JS API for Firestore](https://firebase.google.com/docs/reference/js/firebase.firestore). This means all of the methods regularly available through `firebase.firestore()` and the statics available from `firebase.firestore` are available. Certain methods (such as `get`, `set`, and `onSnapshot`) have a different API since they have been extended with action dispatching. The methods which have dispatch actions are listed below:
168+
169+
#### Actions
170+
171+
##### get
172+
```js
173+
store.firestore.get({ collection: 'cities' }),
174+
// store.firestore.get({ collection: 'cities', doc: 'SF' }), // doc
175+
```
176+
177+
##### set
178+
```js
179+
store.firestore.set({ collection: 'cities', doc: 'SF' }, { name: 'San Francisco' }),
180+
```
181+
182+
##### add
183+
```js
184+
store.firestore.add({ collection: 'cities' }, { name: 'Some Place' }),
185+
```
186+
187+
##### update
188+
```js
189+
const itemUpdates = {
190+
some: 'value',
191+
updatedAt: store.firestore.FieldValue.serverTimestamp()
192+
}
193+
194+
store.firestore.update({ collection: 'cities', doc: 'SF' }, itemUpdates),
195+
```
196+
197+
##### delete
198+
```js
199+
store.firestore.delete({ collection: 'cities', doc: 'SF' }),
200+
```
201+
202+
##### runTransaction
203+
```js
204+
store.firestore.runTransaction(t => {
205+
return t.get(cityRef)
206+
.then(doc => {
207+
// Add one person to the city population
208+
const newPopulation = doc.data().population + 1;
209+
t.update(cityRef, { population: newPopulation });
210+
});
211+
})
212+
.then(result => {
213+
// TRANSACTION_SUCCESS action dispatched
214+
console.log('Transaction success!');
215+
}).catch(err => {
216+
// TRANSACTION_FAILURE action dispatched
217+
console.log('Transaction failure:', err);
218+
});
219+
```
167220

168221
#### Types of Queries
169222

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.5.0",
3+
"version": "0.5.1",
44
"description": "Redux bindings for Firestore.",
55
"main": "lib/index.js",
66
"module": "es/index.js",

src/actions/firestore.js

+22
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,27 @@ export function unsetListeners(firebase, dispatch, listeners) {
364364
});
365365
}
366366

367+
/**
368+
* Atomic operation with Firestore (either read or write).
369+
* @param {Object} firebase - Internal firebase object
370+
* @param {Function} dispatch - Redux's dispatch function
371+
* @param {Function} transactionPromise - Function which runs transaction
372+
* operation.
373+
* @return {Promise} Resolves with result of transaction operation
374+
*/
375+
export function runTransaction(firebase, dispatch, transactionPromise) {
376+
return wrapInDispatch(dispatch, {
377+
ref: firebase.firestore,
378+
method: 'runTransaction',
379+
args: [transactionPromise],
380+
types: [
381+
actionTypes.TRANSACTION_START,
382+
actionTypes.TRANSACTION_SUCCESS,
383+
actionTypes.TRANSACTION_FAILURE,
384+
],
385+
});
386+
}
387+
367388
export default {
368389
get,
369390
firestoreRef,
@@ -373,4 +394,5 @@ export default {
373394
setListeners,
374395
unsetListener,
375396
unsetListeners,
397+
runTransaction,
376398
};

src/constants.js

+6
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ export const actionsPrefix = '@@reduxFirestore';
3939
* @property {String} ON_SNAPSHOT_REQUEST - `@@reduxFirestore/ON_SNAPSHOT_REQUEST`
4040
* @property {String} ON_SNAPSHOT_SUCCESS - `@@reduxFirestore/ON_SNAPSHOT_SUCCESS`
4141
* @property {String} ON_SNAPSHOT_FAILURE - `@@reduxFirestore/ON_SNAPSHOT_FAILURE`
42+
* @property {String} TRANSACTION_START - `@@reduxFirestore/TRANSACTION_START`
43+
* @property {String} TRANSACTION_SUCCESS - `@@reduxFirestore/TRANSACTION_SUCCESS`
44+
* @property {String} TRANSACTION_FAILURE - `@@reduxFirestore/TRANSACTION_FAILURE`
4245
* @example
4346
* import { actionTypes } from 'react-redux-firebase'
4447
* actionTypes.SET === '@@reduxFirestore/SET' // true
@@ -75,6 +78,9 @@ export const actionTypes = {
7578
DOCUMENT_ADDED: `${actionsPrefix}/DOCUMENT_ADDED`,
7679
DOCUMENT_MODIFIED: `${actionsPrefix}/DOCUMENT_MODIFIED`,
7780
DOCUMENT_REMOVED: `${actionsPrefix}/DOCUMENT_REMOVED`,
81+
TRANSACTION_START: `${actionsPrefix}/TRANSACTION_START`,
82+
TRANSACTION_SUCCESS: `${actionsPrefix}/TRANSACTION_SUCCESS`,
83+
TRANSACTION_FAILURE: `${actionsPrefix}/TRANSACTION_FAILURE`,
7884
};
7985

8086
/**

src/createFirestoreInstance.js

+9-13
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,13 @@ export default function createFirestoreInstance(firebase, configs, dispatch) {
3636
aliases,
3737
);
3838

39-
// Attach helpers to specified namespace
40-
if (configs.helpersNamespace) {
41-
return {
42-
...firebase,
43-
...firebase.firestore,
44-
[configs.helpersNamespace]: methods,
45-
};
46-
}
47-
return {
48-
...firebase,
49-
...firebase.firestore,
50-
...methods,
51-
};
39+
return Object.assign(
40+
firebase && firebase.firestore ? firebase.firestore() : {},
41+
firebase.firestore,
42+
{ _: firebase._ },
43+
configs.helpersNamespace
44+
? // Attach helpers to specified namespace
45+
{ [configs.helpersNamespace]: methods }
46+
: methods,
47+
);
5248
}

src/reducers/orderedReducer.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ function writeCollection(collectionState, action) {
9393
}
9494

9595
if (meta.doc && size(collectionState)) {
96-
// Update item in array (handling storeAs)
97-
return updateItemInArray(collectionState, meta.storeAs || meta.doc, item =>
96+
// Update item in array
97+
return updateItemInArray(collectionState, meta.doc, item =>
9898
mergeObjects(item, action.payload.ordered[0]),
9999
);
100100
}

src/utils/actions.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ function makePayload({ payload }, valToPass) {
2323
*/
2424
export function wrapInDispatch(
2525
dispatch,
26-
{ ref, meta, method, args = [], types },
26+
{ ref, meta = {}, method, args = [], types },
2727
) {
2828
const [requestingType, successType, errorType] = types;
2929
dispatch({

test/unit/actions/firestore.spec.js

+120
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,70 @@ describe('firestoreActions', () => {
536536
'Listeners must be an Array of listener configs (Strings/Objects).',
537537
);
538538
});
539+
540+
describe('oneListenerPerPath', () => {
541+
it('works with one listener', async () => {
542+
const fakeFirebaseWithOneListener = {
543+
_: {
544+
listeners: {},
545+
config: { ...defaultConfig, oneListenerPerPath: true },
546+
},
547+
firestore: () => ({
548+
collection: collectionClass,
549+
}),
550+
};
551+
const instance = createFirestoreInstance(
552+
fakeFirebaseWithOneListener,
553+
{ helpersNamespace: 'test' },
554+
dispatchSpy,
555+
);
556+
const listeners = [
557+
{
558+
collection: 'test',
559+
doc: '1',
560+
subcollections: [{ collection: 'test2' }],
561+
},
562+
];
563+
const forEachMock = sinon.spy(listeners, 'forEach');
564+
await instance.test.setListeners(listeners);
565+
expect(forEachMock).to.be.calledOnce;
566+
// SET_LISTENER, LISTENER_RESPONSE, LISTENER_ERROR
567+
expect(dispatchSpy).to.be.calledThrice;
568+
});
569+
570+
it('works with two listeners of the same path (only attaches once)', async () => {
571+
const fakeFirebaseWithOneListener = {
572+
_: {
573+
listeners: {},
574+
config: { ...defaultConfig, oneListenerPerPath: true },
575+
},
576+
firestore: () => ({
577+
collection: collectionClass,
578+
}),
579+
};
580+
const instance = createFirestoreInstance(
581+
fakeFirebaseWithOneListener,
582+
{ helpersNamespace: 'test' },
583+
dispatchSpy,
584+
);
585+
const listeners = [
586+
{
587+
collection: 'test',
588+
doc: '1',
589+
subcollections: [{ collection: 'test3' }],
590+
},
591+
{
592+
collection: 'test',
593+
doc: '1',
594+
subcollections: [{ collection: 'test3' }],
595+
},
596+
];
597+
const forEachMock = sinon.spy(listeners, 'forEach');
598+
await instance.test.setListeners(listeners);
599+
expect(forEachMock).to.be.calledOnce;
600+
expect(dispatchSpy).to.be.calledThrice;
601+
});
602+
});
539603
});
540604

541605
describe('unsetListener', () => {
@@ -586,6 +650,62 @@ describe('firestoreActions', () => {
586650
type: actionTypes.UNSET_LISTENER,
587651
});
588652
});
653+
654+
describe('oneListenerPerPath option enabled', () => {
655+
it('dispatches UNSET_LISTENER action', async () => {
656+
const fakeFirebaseWithOneListener = {
657+
_: {
658+
listeners: {},
659+
config: { ...defaultConfig, oneListenerPerPath: true },
660+
},
661+
firestore: () => ({
662+
collection: collectionClass,
663+
}),
664+
};
665+
const instance = createFirestoreInstance(
666+
fakeFirebaseWithOneListener,
667+
{ helpersNamespace: 'test' },
668+
dispatchSpy,
669+
);
670+
await instance.test.unsetListeners([{ collection: 'test' }]);
671+
expect(dispatchSpy).to.have.callCount(0);
672+
});
673+
674+
it('dispatches UNSET_LISTENER action if there is more than one listener', async () => {
675+
const fakeFirebaseWithOneListener = {
676+
_: {
677+
listeners: {},
678+
config: { ...defaultConfig, oneListenerPerPath: true },
679+
},
680+
firestore: () => ({
681+
collection: collectionClass,
682+
}),
683+
};
684+
const instance = createFirestoreInstance(
685+
fakeFirebaseWithOneListener,
686+
{ helpersNamespace: 'test' },
687+
dispatchSpy,
688+
);
689+
await instance.test.setListeners([
690+
{ collection: 'test' },
691+
{ collection: 'test' },
692+
]);
693+
await instance.test.unsetListeners([{ collection: 'test' }]);
694+
expect(dispatchSpy).to.be.calledThrice;
695+
});
696+
});
697+
});
698+
699+
describe('runTransaction', () => {
700+
it('throws if invalid path config is provided', () => {
701+
const instance = createFirestoreInstance(
702+
{},
703+
{ helpersNamespace: 'test' },
704+
);
705+
expect(() => instance.test.runTransaction()).to.throw(
706+
'dispatch is not a function',
707+
);
708+
});
589709
});
590710
});
591711
});

test/unit/enhancer.spec.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const reducer = sinon.spy();
55
const generateCreateStore = () =>
66
compose(
77
reduxFirestore(
8-
{},
8+
{ firestore: () => ({ collection: () => ({}) }) },
99
{
1010
userProfile: 'users',
1111
},
@@ -23,9 +23,13 @@ describe('enhancer', () => {
2323
expect(store).to.have.property('firestore');
2424
});
2525

26-
it('has the right methods', () => {
26+
it('adds extended methods', () => {
2727
expect(store.firestore.setListener).to.be.a('function');
2828
});
29+
30+
it('preserves unmodified internal Firebase methods', () => {
31+
expect(store.firestore.collection).to.be.a('function');
32+
});
2933
});
3034

3135
describe('getFirestore', () => {

test/unit/reducers/orderedReducer.spec.js

+32
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,38 @@ describe('orderedReducer', () => {
254254
orderedData,
255255
);
256256
});
257+
258+
it('updates doc under storeAs', () => {
259+
action = {
260+
type: actionTypes.LISTENER_RESPONSE,
261+
meta: {
262+
collection: 'testing',
263+
doc: '123abc',
264+
storeAs: 'pathName',
265+
},
266+
payload: {
267+
ordered: [
268+
{
269+
content: 'new',
270+
},
271+
],
272+
},
273+
merge: {},
274+
};
275+
276+
state = {
277+
pathName: [
278+
{
279+
id: '123abc',
280+
content: 'old',
281+
},
282+
],
283+
};
284+
expect(orderedReducer(state, action)).to.have.nested.property(
285+
`pathName.0.content`,
286+
'new',
287+
);
288+
});
257289
});
258290

259291
describe('GET_SUCCESS', () => {

0 commit comments

Comments
 (0)