Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/srbrahma/firebase-database-modeler

Take your Firebase Realtime Database to the next level with models!
https://github.com/srbrahma/firebase-database-modeler

cloud-functions firebase firebase-database-modeler npm realtime-database typescript

Last synced: 19 days ago
JSON representation

Take your Firebase Realtime Database to the next level with models!

Awesome Lists containing this project

README

        

# Firebase Database Modeler

[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)
[![TypeScript](https://badgen.net/npm/types/env-var)](http://www.typescriptlang.org/)
[![npm](https://img.shields.io/npm/v/firebase-database-modeler)](https://www.npmjs.com/package/firebase-database-modeler)
[![npm](https://img.shields.io/npm/dt/firebase-database-modeler)](https://www.npmjs.com/package/firebase-database-modeler)

Firebase Database Modeler upgrades your Realtime Database to a whole new level!

Full and awesome Typescript support!

Supports firebase, firebase-admin and react-native-firebase packages.

README still being improved. Not a focus right now, as I am using this package in a full time real project development.

# Instalation

```js
npm install --save firebase-database-modeler
// or
yarn add firebase-database-modeler
```

# Usage

```typescript
import { _, _$, _root, modelerSetDefaultDatabase } from 'firebase-database-modeler';

// There are multiple ways of setting up the database depending of the firebase package
// you are using (firebase, firebase-admin or react-native-firebase).
// Read their docs to see how to get the firebase.database().
const database = firebase.database()

modelerSetDefaultDatabase(database);

const stores = _('stores', {
$storeId: _$({
name: _('n'), // The DB property key can be different from the model property key!
rating: _('rating'),
open: _('open'),
optionalProp: _('oP'), // You can tag the model property as optional by adding `| null`.
users: _('users', {
$userId: _$({
name: _('name')
})
})
})
})

const root = _root({
stores
})

async function createStore(storeId: string, userId: string, userName: string) {
// Typescript IntelliSense will fully guide you to build the object!
// In _set(), all the model properties are required.
await stores.$storeId._set({
name: 'Cool Store', // In the model declaration, we've set this name property
// to have the key 'n' in the DB. _set() automatically converts this!
rating: 4.2,
open: true,
users: {
[userId]: {
name: userName
};
}
}, storeId) // This storeId variable will be used as the $storeId path segment
}

async function setStoreName(storeId: string, newName: string) {
// Typescript will complain if the _set() first argument is not a string, in this case.
await stores.$storeId._set(newName, storeId)
}

// The type of this function will be the store model type! This package
// automatically converts the model schema to the DB schema!
async function getStore(storeId: string) {
return await stores.$storeId._onceVal('value', storeId)
}

```

# API


## Functions


modelerSetDefaultDatabase (database: Database) => void

Sets the default database that will be used by all Realtime Database operations you may call using your Model.

You do not need to use this if you are passing the database to the `_root()` or `._ref()` based functions.


\_ (key: string, children?: Node) => Node

Creates a Node. First parameter is the Node key: the name of it in the database.

The second parameter allows Node nesting.

You may pass a type to it.

```typescript
const root = _('/', {
first: _('1st'),
second: _('second', {
nested: _('stuff'),
}),
});

database.second.nested._key(); // = 'stuff'
```


\_\$ (key: string, children?: Node) => Node

Creates a Variable Node. It's the same as calling `_('$', children)`.

```typescript
const users = _('users', {
$userId: _$({
name: _('name'),
age: _('age'),
})
})
```


\_root (key: string, children?: Node, database?: Database, blockDatabase: boolean = false) => Node

Creates a Root Node. You MUST call this to your Model root to make everything work.

If you use the `database` parameter, it will apply it recursively to all Model Nodes (to the ._database property), having preference over the database that can be set with the `modelerSetDefaultDatabase()` but can be overriden by the `database` parameter in `._ref()` based functions.

If `blockDatabase == true`, an Error will be throw if used the `database` parameter in `._ref()` based functions. This is useful and safe if using more than one Model and one of them uses the `database` parameter in `._ref` and the other doesn't, so, you won't mess your DB by a mistake.

```typescript
const root = _root({
users:
name: _('name'),
age: _('age'),
});
```

pathSegmentIsValid (segment: string) => boolean

A path segment is 'each/part/of/a/path', separated by '/'. This function checks if the given segment is a string, and if matches the RegEx `/^[a-zA-Z0-9_-]+$/` . Useful to check if the client/server is using a valid and safe path.

This is automatically called by `_pathWithVars` (see below)


## Node properties


\._key : string

Returns the value you entered as the first argument of the \_ function.
Is the last part of the path.

```typescript
// E.g.:
users.$userId.stores._key; // Returns 'stores'
```


\._path : string

Returns the entire path (hierarchical concatenation of all keys / segments). '\$' keys variables aren't converted.

This property is recursively set when you call the `_root({yourModel})`, and that's why we have to call it.

```typescript
// E.g.:
stores.$storeId.users.$userId.name._path; // Returns 'stores/$/users/$/name'
```


\._pathWithVars (vars?: string | string[]) => string

Returns the path with **'\$'** variables converted. For each Variable Node, you must pass
its string value as parameter. Each `vars` item is tested with the `pathSegmentIsValid` function.

If you passed a number of variables lesser than the required or any of them have an invalid value (not a string or the string doesn't match the Regex `/^[a-zA-Z0-9_-]+$/`), an error with useful information will be throw. This will also happen in any other function here that calls uses this one.

```typescript
// E.g.:
stores.$storeId.users.$userId_pathWithVars(['abc', '0xDEADBEEF']); // Returns 'stores/abc/users/0xDEADBEEF
```


\._pathTo (targetNode: Node, vars?: string | string[]) => string

Returns the path from the current node to the given target node. If the target node is not a child of any level of the current node, an error is thrown. _pathWithVars(...vars) is executed. The current node key / segment isn't included in the result, but is the target node.

The `vars` here is relative: You must only pass the vars that are after the model you called the _pathTo. Example below.

This method is very useful in a update() function as the object dynamic key. Example below.

```typescript
// E.g.:

const m$storeId = stores.$storeId; // Just to reduce code size. This 'm' in the start of the const stands for model. I use this "standard" in my codes.

m$storeId._pathTo(m$storeId.users.$userId); // Returns 'users/$'

m$storeId._pathTo(m$storeId.users.$userId, 'xyz'); // Returns 'users/xyz'. Notice that the vars 'xyz' is for the $userId and not for the $storeId.

// Example to show its functionality in update(). This example will change at the same time both users names. We do not use _update() as we are not following the object model properties directly.
m$storeId._ref('store1').update({
[m$storeId._pathTo(m$storeId.users.$userId.name, 'user1')]: 'John', // _pathTo result is 'users/user1/name'
[m$storeId._pathTo(m$storeId.users.$userId.name, 'user2')]: 'Anna', // _pathTo result is 'users/user2/name'
})
```


\._dbType : ModelLikeDbData

Use it with Typescript `typeof` to get the ModelLikeDbData type of the node. Its real value is undefined, so, only useful for getting the type.

**ModelLikeDbData** is a type that is almost like to the DB schema, but with the property keys still being the model ones. `~'$variableNodes: (childrenNodesType)'` types are converted to `~'[x: string]: (childrenNodesType)'`. You will read this type name a few times in this README.

You probably won't use this property directly.


\._ref (vars?: string | string[], database?: Database) => Reference

Returns a Realtime Database reference while using the same working of \_pathWithVars.

You may pass a database as argument. It has preference over the database set by `modelerSetDefaultDatabase()` or by the `_root()` or `._clone()` database argument. (Read in `_root` about `blockDatabase`.)

It is called by all the DB operations methods that will soon appear below.

The `vars` and `database` parameters will appear in another functions, with the same functionality.

```typescript
// E.g.:
stores.$storeId.rating._ref('abc').set(2.7);
```


\._dataToDb (data: ModelLikeDbData) => any

Converts the inputted data to your Realtime Database schema, the exact way that will appear in your DB.


\._dataFromDb (data: any) => ModelLikeDbData

Converts data from the DB (received with ref.on() or ref.once()) to a Model-Like object, with typings.


\._onceVal (event: EventType, vars?: string | string[], database?: Database) => ModelLikeDbData

A simple way to retrieve data from the DB once.

Same as `model._dataFromDb(await model.\_ref(vars).once(event)).val()`.


\._onVal (event: EventType, callback: (data: ModelLikeDbData) => void, vars?: string | string[], database?: Database) => Reference

Like Firebase `ref.on()`, it will execute the callback for every time the event happens. This one will also execute `model._dataFromDb(snapshot.val())` in the snapshot.


\._exists (vars?: string | string[], database?: Database) => Promise\

Returns if the reference exists.
Same as `(await model._ref(vars).once('value')).exists()`

```typescript
// E.g.:
await stores.$storeId.rating._exists(); // Will return true or false.
```


\._set (value: ModelLikeDbData, vars?: string | string[], database?: Database) => Promise\

Same as `model._ref(vars).set(model._dataToDb(value))`, with type checking on value.


\._update (value: Partial\, vars?: string | string[], database?: Database) => Promise\

Same as `model._ref(vars).update(model._dataToDb(value))`, with type checking on value.

The value or its children are all optional/undefined, as `update` in RTDB only changes the defined properties and keeps the current value of the undefined ones.

Also, you may now (2.8.0) pass `null` to optional properties to remove the current value.


\._push (value: ModelLikeDbData, vars?: string | string[], database?: Database) => Promise\

Same as `model._ref(vars).push((model._dataToDb(value)))`, with type checking on value.

With the same working of ref().push(), you may pass undefined as the `value` to just create the reference (to access the client side `key` property), without actually storing the new data. To learn more about it, Google about push() with or without arguments!

If the child of the used model is a Variable Node, the `value` type will smartly be `~'ModelLikeDbData'` ( = if your model is stores/$storeId/... and you call stores._push(), the type annotation will be the $storeId type)

```typescript
// E.g.:
const newStoreId = (await stores._push({
name: 'Cool Store',
rating: 4.2,
open: true,
users: {
[aUserId]: {
name: theUserName
};
}
})).key! // ! because the type of Reference.key is (string | null), but we know that in this case it is a string

stores.$storeId.name._ref(newStoreId).set('New Name!') // Changes 'Cool Store' to 'New Name!'
```


\._clone (vars?: string | string[], database?: Database, blockDatabase: boolean = false) => Node

Deep clones the Model Node applying `vars` to the '`\$`' keys to the new cloned model `._path`. Useful for not having to pass the `vars` all the time to a Model that you will use for a while, like having it in a Class.

`database` and `blockDatabase` works as the same `_root` parameters.



._database : Database | undefined

If you passed the `database` argument in `_root()` or in `_clone()`, it will be set in this property.

You probably won't have to use this.



._blockDatabase : boolean

If you passed the `blockDatabase` argument in `_root()` or in `_clone()`, it will be set in this property.

You probably won't have to use this.



# Roadmap

- Optional properties. For now, you may use the type null and pass a null value. For VarNodes, you may pass an empty object.

- Optional database key; it would use the property key as the database key, getting them on finishModel().

- Firestore support. Easy to add, but I don't think I will ever use Firestore again (its max 1 write per second is a big limitation).

- Code tests

- Check if there is a child with the same DB key

- Improve this README

- Typescript sourcery to know how many `vars` are needed for current node DB op

- ._updateChild() will from the given object construct the key / path and only update the given child.

- If blockDatabase == true, hide `database` property from `._ref()` based functions.

- ._onceVal() overload with implicit 'value'.

- Automatically add general use references like `'.info/connected'` to the `_root()` Node (https://firebase.google.com/docs/database/web/offline-capabilities#section-connection-state)

- Check if path === '' on `._ref`-like functions (= not called `_root()`)


# Attention!

### Model object reuse

Don't use the same model object in more than one place! See example.

```typescript
const modelObj = {
prop: _('prop')
};
const root = _root({
model1: _('segment1', model),
model2: _('segment2', model),
})
root.model1.prop._path()
// This will return /segment2/prop instead of /segment1/prop, because the path
// applied to model2.prop was also applied to model1.prop, as they are the same object.
// This "same path" behavior applies to any DB operation you would do.
```

To avoid this issue, you can either create a modelObj2 with the same content, or use the `deepClone` function that this package exports. It just deep clones an object.

### Not allowed characters

Your Realtime Database paths / segments must not include '$' char and it's not recomended to a segment to start with an '_', as those chars are specially treated by this package.

For this package model keys, only the '_' recommendation remains ( = you may use the '$'. Actually, is recommended to use it to indicate that it is a Variable Node).

IDs generated by Firebase Auth and Realtime Database reference.push() don't include '$' and doesn't start with an '_'.

# [Changelog](https://github.com/SrBrahma/Firebase-Database-Modeler/blob/master/CHANGELOG.md)