Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add experimental support for realtime database and authentication #4

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
5 changes: 5 additions & 0 deletions .firebaserc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"projects": {
"default": "todo-bd3e8"
}
}
10 changes: 5 additions & 5 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"semi": true,
"trailingComma": "all",
"singleQuote": true,
"printWidth": 120,
"tabWidth": 4
"semi": true,
"trailingComma": "all",
"singleQuote": true,
"printWidth": 120,
"tabWidth": 2
}
207 changes: 182 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![NPM version](https://img.shields.io/npm/v/firemelon)](https://www.npmjs.com/package/firemelon?activeTab=versions)
[![Test](https://github.com/AliAllaf/firemelon/workflows/Test/badge.svg)](https://github.com/AliAllaf/firemelon)

A simple way to sync between WatermelonDB and Firestore.
A simple way to sync between WatermelonDB and Firestore/Realtime Database (Experimental).

## Installation

Expand All @@ -25,50 +25,207 @@ Firemelon works with both [@firebase/firestore](https://www.npmjs.com/package/@f

## Usage

```typescript
import { sync, SyncConfig, sessionId } from 'firemelon';

const syncConfig: SyncConfig = {
objects: ['todos'],
db: firebase.firestore(),
storage: 'firestore',
getTimestamp: () => new Date(),
};

await sync(watermelonDatabase, sessionId(), syncConfig);
```

---

```typescript
const sync = async (database: Database, sessionId: string, config: SyncConfig);
```

- **database** :
The _WatermelonDB_ database to be synced.

- **sessionId** :
A unique ID for each session to prevent the function from pulling its own pushed changes.

- **config**: The sync configuration object

---

### SyncConfig

```typescript
interface SyncConfig {
objects: SyncObj;
storage: SyncStorage;
getTimestamp?: () => any;
authenticated?: boolean;
getUserId?: () => Promise<string>;
db: FirestoreModule | Database;
excludedFields?: string[];
}
```

- **objects** :
Refers to which collections should be synced from Watermelon DB to Firebase.
Can either be an array or a `SyncObj`.

- **storage**:
Must be either `firestore` or `realtime-database`. Note: `relatime-database` support is currently experimental.

- **db** :
The _firestore_ or _realtime database_ module used in the app.

- **authenticated**:
When enabled, the `sync` function will throw an error unless a valid userId is retrieved from the getUserId function.

- **getUserId**:
Returns a string value for the current userId. All written data will be scoped to this user and fetched data too.

- **getTimestamp**:
A custom function to provide a date for the sync time.
default is `new Date()`.

This is an example of a more accurate way :

```typescript
const timefn = () => firestore.FieldValue.serverTimestamp();
```

- **excludedFields**:
Fields to exclude from _ALL_ synchronized objects. If you would lie to exclude fields on a per-collection basis use the `SyncObj`.

---

### SyncObj

```typescript
export type SyncObj =
| {
[collectionName: string]: SyncCollectionOptions;
}
| string[];
```

Example usage:

```typescript
// 1. A simple array, do not require any configuration
const objects: SyncObj = ['todos', 'users', 'feeds'];

// 2. An object, with each collection key and custom configuration
const objects: SyncObj = {
todos: {},
users: {
excludedFields: ['email'],
},
feeds: {
customQuery: firestore().collection('feeds').where('isSpecial', '==', true),
},
};
```

---

### Older Usage

This is how the library was accessed in older versions, the same interface is still exported for reverse-compatibility.
You can interact with it using either interface.

```typescript
import { syncFireMelon } from 'firemelon';

async () => {
await syncFireMelon(database, syncObject, firestore, sessionId, timefn());
await syncFireMelon(database, syncObject, firestore, sessionId, timefn());
};
```

- **database** :
The _WatermelonDB_ database to be synced.
- **database** :
The _WatermelonDB_ database to be synced.

- **syncObject** :
An object in which the synced collections and there options are
- **syncObject** :
An object in which the synced collections and there options are

### Example:

```typescript
const syncObject = {
// collections to sync
todos: {
// (optional)
excludedFields: ['color', 'userId'],
// collections to sync
todos: {
// (optional)
excludedFields: ['color', 'userId'],

// To provide extra filters in queries. (optional)
customQuery: firestore.collection('todos').where('color', '==', 'red'),
},
// To provide extra filters in queries. (optional)
customQuery: firestore.collection('todos').where('color', '==', 'red'),
},

users: {},
users: {},
};
```

- **firestore** :
The _Firestore_ module used in the app.
- **firestore** :
The _Firestore_ module used in the app.

- **sessionId** :
A unique ID for each session to prevent the function from pulling its own pushed changes.

- **timefn()** :

A custom function to provide a date for the sync time.
default is `new Date()`.

This is an example of a more accurate way :

```typescript
const timefn = () => firestore.FieldValue.serverTimestamp();
```

## Realtime Database Support (Experimental)

Realtime database support is currently experimental, under active development.
If you wish to use the realtime database, update your `syncConfig` accordingly.

**Why use Realtime Database?**

Unlike Cloud Firestore, Realtime DB does not charge per operation (read/write/delete) rather they charge for total bandwidth and data stored.

If your app requires frequent synchronization across several clients, you could be looking at a large number of reads and writes per day or hour, depending on the amount of data stored and the amount of devices being synced to. This could end up costing quite a bit.

So using the Realtime Database might be a better alternative. However, there are some limitations:

- All of the synchronized data has to be stored under one object for a given user (see below)

```json
{
"sync": {
"user1": {
"todos": {...},
"feeds": {...},
"lists": {...}
},
"user2": {
"todos": {...},
"feeds": {...},
"lists": {...}
}
}
}
```

- Every time you call `sync` the entire dataset for a given user must be downloaded. So if you have 10mb worth of data stored in `sync/user1/**` that must be downloaded in order to write any new data to that location. This may affect performance seriously with large datasets.
- Timestamps must be stored as `numbers` and note a `Date` otherwise the data will fail to save properly
- You cannot use `customQueries` to apply additional filtering to the pulled changes. All data stored will be synchronized across all devices.

- **sessionId** :
A unique ID for each session to prevent the function from pulling its own pushed changes.
**Which one should I use?**

- **timefn()** :
Considering Realtime Database support is experimental, I would suggest using Firestore for now.
If you anticipate a high number of monthly read/write/delete operations or high-frequency synchronization across several devices, the RTDB might be a better shot.

A custom function to provide a date for the sync time.
default is `new Date()`.
## Authentication

This is an example of a more accurate way :
Support for authentication is also experimental.
If you pass a `getUserId` function to the `syncConfig` and set `authenticated` to true, the synchronized data will be scoped to the user with the given userId.

```typescript
const timefn = () => firestore.FieldValue.serverTimestamp();
```
It's up to you to decide where to get the userId from, this can be the firebase user UID or from another authentication provider (Auth0, AWS Cognito, Next Auth etc.)
2 changes: 2 additions & 0 deletions database-debug.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
15:15:52.546 [NamespaceSystem-akka.actor.default-dispatcher-5] INFO akka.event.slf4j.Slf4jLogger - Slf4jLogger started
15:15:52.763 [main] INFO com.firebase.server.forge.App$ - Listening at localhost:9000
7 changes: 7 additions & 0 deletions database.rules.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
/* Visit https://firebase.google.com/docs/database/security to learn more about security rules. */
"rules": {
".read": true,
".write": true
}
}
9 changes: 9 additions & 0 deletions firebase.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"database": {
"rules": "database.rules.json"
},
"firestore": {
"rules": "firestore.rules",
"indexes": "firestore.indexes.json"
}
}
4 changes: 4 additions & 0 deletions firestore.indexes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"indexes": [],
"fieldOverrides": []
}
8 changes: 8 additions & 0 deletions firestore.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true
}
}
}
Loading