|
| 1 | +## Contributing guide |
| 2 | + |
| 3 | +This is an in-progress guide to help guide you in understanding how Firestack works with the goal to help on-board your contributions. If you have any questions, comments, or concerns, feel free to leave it here or join the [gitter channel at https://gitter.im/fullstackreact/react-native-firestack](https://gitter.im/fullstackreact/react-native-firestack). |
| 4 | + |
| 5 | +## Contribution methods |
| 6 | + |
| 7 | +Contributing is easy. Make a fork of the project on [github](https://github.com/fullstackreact/react-native-firestack). Clone this repo on your machine and work on the edits there. |
| 8 | + |
| 9 | +```shell |
| 10 | +git clone https://github.com/[your_name]/react-native-firestack.git |
| 11 | +cd react-native-firestack |
| 12 | +npm install |
| 13 | +``` |
| 14 | + |
| 15 | +We have an [Example app - FirestackApp](https://github.com/fullstackreact/FirestackApp) which we use to demonstrate and test features (until we can get a proper testing environment). Currently, our workflow looks like this: |
| 16 | + |
| 17 | +1. Write JS/native feature |
| 18 | +2. `rsync` the local library to your `node_modules` directory (react-native does not play well with symlinks). |
| 19 | +For instance, running the following in the firestackApp root directory. Make sure you replace the `~/Development/react-native/mine/react-native-firestack` with the path of your cloned repo on your drive: |
| 20 | + |
| 21 | +```javascript |
| 22 | +rsync -avhW --delete \ |
| 23 | + --exclude='node_modules' \ |
| 24 | + --exclude='.git' \ |
| 25 | + --exclude='coverage' \ |
| 26 | + ~/Development/react-native/mine/react-native-firestack/ \ |
| 27 | + ./node_modules/react-native-firestack/ |
| 28 | +``` |
| 29 | + |
| 30 | +3. Test in-app |
| 31 | +4. Update README.md with bugfix/feature |
| 32 | +5. Create a pull request (PR) |
| 33 | + |
| 34 | +## High level |
| 35 | + |
| 36 | +## How it works technically |
| 37 | + |
| 38 | +Firestack is broken up by functional modules which control/interact with the different features of Firebase. I.e. there is a database module, which maps to the Real-Time Database feature in Firebase, Analytics maps to the Firebase analytics stack. |
| 39 | + |
| 40 | +When the user creates a new instance of Firestack, they are creating an instance of the JS class defined in `lib/firestack.js`. |
| 41 | + |
| 42 | +```javascript |
| 43 | +// This creates a JS instance of the |
| 44 | +// Firestack class |
| 45 | +const firestack = new Firestack({}); |
| 46 | +``` |
| 47 | + |
| 48 | +Each of the modules in Firestack can be accessed through this instance. For instance, when we want to access the real-time database through the `firestack` instance, the JS API exposes a `database` accessor. |
| 49 | + |
| 50 | +For instance, when interacting with the database from the instance above, we would call `.database` to get access to a singleton instance of the JS `Database` class defined in `lib/modules/database.js`. |
| 51 | + |
| 52 | +### Database walk-through |
| 53 | + |
| 54 | +```javascript |
| 55 | +const db = firestack.database; |
| 56 | +``` |
| 57 | + |
| 58 | +The `lib/modules/database.js` file exports two classes, one called `Database` and the other called `DatabaseRef`. Essentially, the `Database` class is a wrapper class that provides a handful of methods to forward off to a `DatabaseRef` instance. |
| 59 | + |
| 60 | +The `DatabaseRef` class defines the actual interaction with the native Firebase SDK. Let's look at the `getAt` method as an example of how the JS side interacts with the native-side and back. |
| 61 | + |
| 62 | +When the user accessess a Firebase ref, the `Database` instance creates a new instance of the `DatabaseRef` JS class. |
| 63 | + |
| 64 | +```javascript |
| 65 | +const ref = db.ref('/events'); |
| 66 | +``` |
| 67 | + |
| 68 | +The `DatabaseRef` class is the wrapper that maps to Firebase database points. For efficiency, the `paths` are stored as an array so we can walk up and down the firebase database using the `parent()` and `child()` methods on a database ref. |
| 69 | + |
| 70 | +Calling `getAt()` on the `ref` (an instance of the `DatabaseRef` class) will make a call to the **native** SDK using a method called `promisify()` |
| 71 | + |
| 72 | +```javascript |
| 73 | +class DatabaseRef { |
| 74 | + // ... |
| 75 | + getAt(key) { |
| 76 | + let path = this.path; |
| 77 | + if (key && typeof(key) == 'string') { |
| 78 | + path = `${path}${separator}${key}` |
| 79 | + } |
| 80 | + return promisify('onOnce', FirestackDatabase)(path); |
| 81 | + } |
| 82 | +} |
| 83 | +``` |
| 84 | + |
| 85 | +Ignoring the first few lines (which are helpers to add to the `path`, which we'll look at shortly), the `promisify()` function (defined in `lib/promisify.js`) takes two arguments: |
| 86 | + |
| 87 | +1. The 'string' name of the native function to call |
| 88 | +2. The native module we want to call it on |
| 89 | + |
| 90 | +The `promisify()` function returns a function that returns a `Promise` object in JS. This returned function calls the native function with a React-Native callback. When the React Native function calls the callback function, the Promise is resolved. |
| 91 | + |
| 92 | +Getting back to the Database example, the `getAt()` function (which has an alias of `get`) calls the `onOnce` function on the `FirestackDatabase` native module. Each platform has their own native module version for each feature area of Firebase. |
| 93 | + |
| 94 | +Every function on the `DatabaseRef` class is called with the `path` from Firebase as well as it's other options. |
| 95 | + |
| 96 | +Let's look at the `onOnce` function of the iOS version of `FirestackDatabase` implemented in `ios/Firestack/FirestackDatabase.m`: |
| 97 | + |
| 98 | +``` |
| 99 | +// This might differ from the current code, but |
| 100 | +// is implemented this way at the time of the writing |
| 101 | +// of this document |
| 102 | +RCT_EXPORT_METHOD(onOnce:(NSString *) path |
| 103 | + name:(NSString *) name |
| 104 | + callback:(RCTResponseSenderBlock) callback) |
| 105 | +{ |
| 106 | + int eventType = [self eventTypeFromName:name]; |
| 107 | + |
| 108 | + FIRDatabaseReference *ref = [self getRefAtPath:path]; |
| 109 | + [ref observeSingleEventOfType:eventType |
| 110 | + withBlock:^(FIRDataSnapshot * _Nonnull snapshot) { |
| 111 | + callback(@[[NSNull null], [self snapshotToDict:snapshot]]); |
| 112 | + } |
| 113 | + withCancelBlock:^(NSError * _Nonnull error) { |
| 114 | + NSLog(@"Error onDBEventOnce: %@", [error debugDescription]); |
| 115 | + callback(@[@{ |
| 116 | + @"error": @"onceError", |
| 117 | + @"msg": [error debugDescription] |
| 118 | + }]); |
| 119 | + }]; |
| 120 | +} |
| 121 | +``` |
| 122 | + |
| 123 | +Every native function (in either iOS or Android) is expected to accept a single callback as the final argument. The `onOnce` function accepts the path (as the first argument) and the name of the event we're interested in (such as `value`) and uses the Native SDK to set up the appropriate functionality. When the function has been called and completed, the callback is called with an error on failure and with success on success. |
| 124 | + |
| 125 | +> An error response is considered one which the first argument is non-null. Therefore, to send a successful response, the first value when calling the callback should be null to indicate success. |
| 126 | +
|
| 127 | +## Adding functionality |
| 128 | + |
| 129 | +// TODO |
0 commit comments