Skip to content

Commit

Permalink
v0.5.1
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
prescottprue authored May 6, 2018
2 parents a2764a7 + c2a789d commit b7d4a4b
Show file tree
Hide file tree
Showing 11 changed files with 254 additions and 21 deletions.
55 changes: 54 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@ For more information [on using recompose visit the docs](https://github.com/acdl
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { isEqual } from 'lodash'
import { watchEvents, unWatchEvents } from './actions/query'
import { getEventsFromInput, createCallable } from './utils'

Expand Down Expand Up @@ -164,6 +163,60 @@ export default connect((state) => ({
todos: state.firestore.ordered.todos
}))(Todos)
```
### API
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:

#### Actions

##### get
```js
store.firestore.get({ collection: 'cities' }),
// store.firestore.get({ collection: 'cities', doc: 'SF' }), // doc
```

##### set
```js
store.firestore.set({ collection: 'cities', doc: 'SF' }, { name: 'San Francisco' }),
```

##### add
```js
store.firestore.add({ collection: 'cities' }, { name: 'Some Place' }),
```

##### update
```js
const itemUpdates = {
some: 'value',
updatedAt: store.firestore.FieldValue.serverTimestamp()
}

store.firestore.update({ collection: 'cities', doc: 'SF' }, itemUpdates),
```

##### delete
```js
store.firestore.delete({ collection: 'cities', doc: 'SF' }),
```

##### runTransaction
```js
store.firestore.runTransaction(t => {
return t.get(cityRef)
.then(doc => {
// Add one person to the city population
const newPopulation = doc.data().population + 1;
t.update(cityRef, { population: newPopulation });
});
})
.then(result => {
// TRANSACTION_SUCCESS action dispatched
console.log('Transaction success!');
}).catch(err => {
// TRANSACTION_FAILURE action dispatched
console.log('Transaction failure:', err);
});
```

#### Types of Queries

Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "redux-firestore",
"version": "0.5.0",
"version": "0.5.1",
"description": "Redux bindings for Firestore.",
"main": "lib/index.js",
"module": "es/index.js",
Expand Down
22 changes: 22 additions & 0 deletions src/actions/firestore.js
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,27 @@ export function unsetListeners(firebase, dispatch, listeners) {
});
}

/**
* Atomic operation with Firestore (either read or write).
* @param {Object} firebase - Internal firebase object
* @param {Function} dispatch - Redux's dispatch function
* @param {Function} transactionPromise - Function which runs transaction
* operation.
* @return {Promise} Resolves with result of transaction operation
*/
export function runTransaction(firebase, dispatch, transactionPromise) {
return wrapInDispatch(dispatch, {
ref: firebase.firestore,
method: 'runTransaction',
args: [transactionPromise],
types: [
actionTypes.TRANSACTION_START,
actionTypes.TRANSACTION_SUCCESS,
actionTypes.TRANSACTION_FAILURE,
],
});
}

export default {
get,
firestoreRef,
Expand All @@ -373,4 +394,5 @@ export default {
setListeners,
unsetListener,
unsetListeners,
runTransaction,
};
6 changes: 6 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ export const actionsPrefix = '@@reduxFirestore';
* @property {String} ON_SNAPSHOT_REQUEST - `@@reduxFirestore/ON_SNAPSHOT_REQUEST`
* @property {String} ON_SNAPSHOT_SUCCESS - `@@reduxFirestore/ON_SNAPSHOT_SUCCESS`
* @property {String} ON_SNAPSHOT_FAILURE - `@@reduxFirestore/ON_SNAPSHOT_FAILURE`
* @property {String} TRANSACTION_START - `@@reduxFirestore/TRANSACTION_START`
* @property {String} TRANSACTION_SUCCESS - `@@reduxFirestore/TRANSACTION_SUCCESS`
* @property {String} TRANSACTION_FAILURE - `@@reduxFirestore/TRANSACTION_FAILURE`
* @example
* import { actionTypes } from 'react-redux-firebase'
* actionTypes.SET === '@@reduxFirestore/SET' // true
Expand Down Expand Up @@ -75,6 +78,9 @@ export const actionTypes = {
DOCUMENT_ADDED: `${actionsPrefix}/DOCUMENT_ADDED`,
DOCUMENT_MODIFIED: `${actionsPrefix}/DOCUMENT_MODIFIED`,
DOCUMENT_REMOVED: `${actionsPrefix}/DOCUMENT_REMOVED`,
TRANSACTION_START: `${actionsPrefix}/TRANSACTION_START`,
TRANSACTION_SUCCESS: `${actionsPrefix}/TRANSACTION_SUCCESS`,
TRANSACTION_FAILURE: `${actionsPrefix}/TRANSACTION_FAILURE`,
};

/**
Expand Down
22 changes: 9 additions & 13 deletions src/createFirestoreInstance.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,13 @@ export default function createFirestoreInstance(firebase, configs, dispatch) {
aliases,
);

// Attach helpers to specified namespace
if (configs.helpersNamespace) {
return {
...firebase,
...firebase.firestore,
[configs.helpersNamespace]: methods,
};
}
return {
...firebase,
...firebase.firestore,
...methods,
};
return Object.assign(
firebase && firebase.firestore ? firebase.firestore() : {},
firebase.firestore,
{ _: firebase._ },
configs.helpersNamespace
? // Attach helpers to specified namespace
{ [configs.helpersNamespace]: methods }
: methods,
);
}
4 changes: 2 additions & 2 deletions src/reducers/orderedReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ function writeCollection(collectionState, action) {
}

if (meta.doc && size(collectionState)) {
// Update item in array (handling storeAs)
return updateItemInArray(collectionState, meta.storeAs || meta.doc, item =>
// Update item in array
return updateItemInArray(collectionState, meta.doc, item =>
mergeObjects(item, action.payload.ordered[0]),
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/utils/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function makePayload({ payload }, valToPass) {
*/
export function wrapInDispatch(
dispatch,
{ ref, meta, method, args = [], types },
{ ref, meta = {}, method, args = [], types },
) {
const [requestingType, successType, errorType] = types;
dispatch({
Expand Down
120 changes: 120 additions & 0 deletions test/unit/actions/firestore.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,70 @@ describe('firestoreActions', () => {
'Listeners must be an Array of listener configs (Strings/Objects).',
);
});

describe('oneListenerPerPath', () => {
it('works with one listener', async () => {
const fakeFirebaseWithOneListener = {
_: {
listeners: {},
config: { ...defaultConfig, oneListenerPerPath: true },
},
firestore: () => ({
collection: collectionClass,
}),
};
const instance = createFirestoreInstance(
fakeFirebaseWithOneListener,
{ helpersNamespace: 'test' },
dispatchSpy,
);
const listeners = [
{
collection: 'test',
doc: '1',
subcollections: [{ collection: 'test2' }],
},
];
const forEachMock = sinon.spy(listeners, 'forEach');
await instance.test.setListeners(listeners);
expect(forEachMock).to.be.calledOnce;
// SET_LISTENER, LISTENER_RESPONSE, LISTENER_ERROR
expect(dispatchSpy).to.be.calledThrice;
});

it('works with two listeners of the same path (only attaches once)', async () => {
const fakeFirebaseWithOneListener = {
_: {
listeners: {},
config: { ...defaultConfig, oneListenerPerPath: true },
},
firestore: () => ({
collection: collectionClass,
}),
};
const instance = createFirestoreInstance(
fakeFirebaseWithOneListener,
{ helpersNamespace: 'test' },
dispatchSpy,
);
const listeners = [
{
collection: 'test',
doc: '1',
subcollections: [{ collection: 'test3' }],
},
{
collection: 'test',
doc: '1',
subcollections: [{ collection: 'test3' }],
},
];
const forEachMock = sinon.spy(listeners, 'forEach');
await instance.test.setListeners(listeners);
expect(forEachMock).to.be.calledOnce;
expect(dispatchSpy).to.be.calledThrice;
});
});
});

describe('unsetListener', () => {
Expand Down Expand Up @@ -586,6 +650,62 @@ describe('firestoreActions', () => {
type: actionTypes.UNSET_LISTENER,
});
});

describe('oneListenerPerPath option enabled', () => {
it('dispatches UNSET_LISTENER action', async () => {
const fakeFirebaseWithOneListener = {
_: {
listeners: {},
config: { ...defaultConfig, oneListenerPerPath: true },
},
firestore: () => ({
collection: collectionClass,
}),
};
const instance = createFirestoreInstance(
fakeFirebaseWithOneListener,
{ helpersNamespace: 'test' },
dispatchSpy,
);
await instance.test.unsetListeners([{ collection: 'test' }]);
expect(dispatchSpy).to.have.callCount(0);
});

it('dispatches UNSET_LISTENER action if there is more than one listener', async () => {
const fakeFirebaseWithOneListener = {
_: {
listeners: {},
config: { ...defaultConfig, oneListenerPerPath: true },
},
firestore: () => ({
collection: collectionClass,
}),
};
const instance = createFirestoreInstance(
fakeFirebaseWithOneListener,
{ helpersNamespace: 'test' },
dispatchSpy,
);
await instance.test.setListeners([
{ collection: 'test' },
{ collection: 'test' },
]);
await instance.test.unsetListeners([{ collection: 'test' }]);
expect(dispatchSpy).to.be.calledThrice;
});
});
});

describe('runTransaction', () => {
it('throws if invalid path config is provided', () => {
const instance = createFirestoreInstance(
{},
{ helpersNamespace: 'test' },
);
expect(() => instance.test.runTransaction()).to.throw(
'dispatch is not a function',
);
});
});
});
});
8 changes: 6 additions & 2 deletions test/unit/enhancer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const reducer = sinon.spy();
const generateCreateStore = () =>
compose(
reduxFirestore(
{},
{ firestore: () => ({ collection: () => ({}) }) },
{
userProfile: 'users',
},
Expand All @@ -23,9 +23,13 @@ describe('enhancer', () => {
expect(store).to.have.property('firestore');
});

it('has the right methods', () => {
it('adds extended methods', () => {
expect(store.firestore.setListener).to.be.a('function');
});

it('preserves unmodified internal Firebase methods', () => {
expect(store.firestore.collection).to.be.a('function');
});
});

describe('getFirestore', () => {
Expand Down
32 changes: 32 additions & 0 deletions test/unit/reducers/orderedReducer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,38 @@ describe('orderedReducer', () => {
orderedData,
);
});

it('updates doc under storeAs', () => {
action = {
type: actionTypes.LISTENER_RESPONSE,
meta: {
collection: 'testing',
doc: '123abc',
storeAs: 'pathName',
},
payload: {
ordered: [
{
content: 'new',
},
],
},
merge: {},
};

state = {
pathName: [
{
id: '123abc',
content: 'old',
},
],
};
expect(orderedReducer(state, action)).to.have.nested.property(
`pathName.0.content`,
'new',
);
});
});

describe('GET_SUCCESS', () => {
Expand Down

0 comments on commit b7d4a4b

Please sign in to comment.