firebase-database-modeler
TypeScript icon, indicating that this package has built-in type declarations

2.8.0 • Public • Published

Firebase Database Modeler

PRs Welcome TypeScript npm npm

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

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

Usage

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: _<string>('n'), // The DB property key can be different from the model property key!
    rating: _<number>('rating'),
    open: _<boolean>('open'),
    optionalProp: _<number | null>('oP'), // You can tag the model property as optional by adding `| null`.
    users: _('users', {
      $userId: _$({
        name: _<string>('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.

const root = _('/', {
  first: _('1st'),
  second: _('second', {
    nested: _<string>('stuff'),
  }),
});

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

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

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

const users = _('users', {
  $userId: _$({
    name: _<string>('name'),
    age: _<number>('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.

const root = _root({
  users:
  name: _<string>('name'),
  age: _<number>('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.

// 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.

// 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.

// 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.

// 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.

// 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<boolean>

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

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

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

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


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

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<any>

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<child>' ( = if your model is stores/$storeId/... and you call stores._push(), the type annotation will be the $storeId type)

// 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.

const modelObj = {
  prop: _<string>('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

Package Sidebar

Install

npm i firebase-database-modeler

Weekly Downloads

0

Version

2.8.0

License

ISC

Unpacked Size

61 kB

Total Files

16

Last publish

Collaborators

  • srbrahma