𝚒𝚗𝚝𝚎𝚐𝚛𝚒𝚏𝚢 - Firestore referential integrity via triggers
Tags: showdev, firestore, serverless, javascript
Published: 2019-01-23 16:01:36

---
title: 𝚒𝚗𝚝𝚎𝚐𝚛𝚒𝚏𝚢 - Firestore referential integrity via triggers
published: true
description: 
tags: showdev,firestore,serverless,javascript
---

# Firestore is amazing, but...

Google Cloud Firestore is a serverless NoSQL document database that scales horizontally - which means it adds/removes nodes to serve your database based on demand automagically. It also does some fancy indexing that allows query times to be proportional to result size instead of total data size. So basically if your query returns 10 records, it will take the same time to run if the total data size is 10, 100, 1000 or squillions of records.

It offers an expressive query language, but does have some [limitations](https://firebase.google.com/docs/firestore/query-data/queries#query_limitations) which guarantee O(ResultSet) performance. Also, while designing NoSQL database schemas, we have to often "unlearn" data normalization principles we learnt building relational databases.

For example, say you had a database that records comments made by users who have usernames and profile photos. Traditionally you would have stored a foreign key called userId in the comments table, and performed a "join" to get comments together with usernames and profile photos.

But in a NoSQL schema, data is often denormalized - in this case for example, username and photo are repeated in each comment record for ease of retrieval.

The key question then of course is how are updates to username/photo reflected across all comments made by a user? In the case of Firestore, one could write a Cloud Function [triggered](https://firebase.google.com/docs/functions/firestore-events) by updates to any user record which replicates the update to all comment records.

# 𝚒𝚗𝚝𝚎𝚐𝚛𝚒𝚏𝚢 can help!
 
[𝚒𝚗𝚝𝚎𝚐𝚛𝚒𝚏𝚢](https://github.com/anishkny/integrify) is an npm library that offers pre-canned Firestore triggers that help maintain referential and data integrity in some commonly occurring scenarios.

## Attribute Replication

**Scenario** - Continuing the users/comments example above, you could have a schema like this:

```
  /users/
    userId/
      username
      photoURL

  /comments/
    commentId/
      body
      userId       <-- foreign key
      username     <-- replicated field
      photoURL     <-- replicated field
```

**Solution** - To enforce referential integrity on updates of username/photoURL, simply use:

```js
exports.replUserAttrs = integrify({
  rule: 'REPLICATE_ATTRIBUTES',
  source: {
    collection: 'users',
  },
  targets: [
    {
      collection: 'comments',
      foreignKey: 'userId',
      attributeMapping: { 
        'username': 'username', 
        'photoURL': 'photoURL', 
      },
    },
  ],
});
```

## Stale Reference Deletion

**Scenario** - Say you have an articles collection, where each article can have zero or more comments each with an articleId foreign key. And you want to delete all comments automatically if the corresponding article is deleted.

```
  /articles/
    articleId/
      body
      updatedAt
      isPublished
      ...

  /comments/
    commentId/
      articleId   <-- foreign key
      body
      ...
```

**Solution** - To delete all comments corresponding to a deleted article, use:

```js
exports.delArticleRefs = integrify({
  rule: 'DELETE_REFERENCES',
  source: {
    collection: 'articles',
  },
  targets: [
    {
      collection: 'comments',
      foreignKey: 'articleId',
    },
  ],
});

```

## Count Maintainence

**Scenario** - Say you want to record which users have liked any particular article and also be able to quickly determine how many total likes an article has received.

```
  /likes/
    likeId/
      userId
      articleId    <-- foreign key

  /articles/
    articleId/
      likesCount   <-- aggregate field
```

**Solution** - To maintain a live count of number of likes stored in the corresponding article document, use:

```js
[
  module.exports.incrementLikesCount,
  module.exports.decrementLikesCount,
] = integrify({
  rule: 'MAINTAIN_COUNT',
  source: {
    collection: 'likes',
    foreignKey: 'articleId',
  },
  target: {
    collection: 'articles',
    attribute: 'likesCount',
  },
});
```

Notice that you get two triggers, one to increment and another to decrement the likesCount attributes for every addition or deletion in the likes collection.

# Deploying

𝚒𝚗𝚝𝚎𝚐𝚛𝚒𝚏𝚢 is meant to be used in conjunction with `firebase-functions` and `firebase-admin`. Indeed, they are peerDependencies for 𝚒𝚗𝚝𝚎𝚐𝚛𝚒𝚏𝚢. Typically, your setup would look like:

```js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();
const { integrify } = require('integrify');

integrify({ config: { functions, db } });

// Use integrify here...
```

Then you would deploy the functions returned by 𝚒𝚗𝚝𝚎𝚐𝚛𝚒𝚏𝚢 like any other [Firebase Function](https://firebase.google.com/docs/functions/get-started):

```bash
firebase deploy --only functions
```

# Source Code

Check out the source code, and feel free to open any issues, send out PRs or general comments! 

{% github anishkny/integrify %} 

Thanks for reading ✌️✌️✌️