Community-driven data modifications using Cloud Functions
Tags: firebase, firestore, cloudfunctions, process
Published: 2019-12-18 16:11:26

---
title: Community-driven data modifications using Cloud Functions
published: true
description: A DRY approach to community-driven data changes in your applications.
tags: firebase, firestore, cloudfunctions, process
cover_image: https://thepracticaldev.s3.amazonaws.com/i/4yqrdxvwqe5b5ql67bzx.png
---

## Prelude

I'm working on a side project around my paid client work and weekly [Soulection Tracklists](https://soulectiontracklists.com) publications called [TracksPlayed](https://www.producthunt.com/@kylewelsby/upcoming) (a spin-off from Soulection Tracklists).

With this new project, I'm exploring new technologies and processes to improve features I wish I had in Soulection Tracklists. 

Here is an insight into a process that I think will allow the community to suggest changes to data (I'm also open to suggestions in the comment section if you have other solutions). 

For this; will assume you're using [Firebase FireStore](https://cloud.google.com/firestore/) (a real-time NoSQL database by Google), but can easily be adapted to work with MongoDB and PostgreSQL projects. 

[![Video Preview](https://j.gifs.com/91j1k8.gif)](https://youtu.be/zbEvk46lVzM)


## Suggestions Collection

Let's start off with a database collection called `suggestions` here we will have a structure like the following.

```
{
  "path": "string",
  "key": "string",
  "value": "string",
  "createdAt": "datetime",
  "approvedAt": "datetime",
  "committedAt": "datetime"
}
```

Let's take a moment to explain each part of this schema. 

|property     |type      |description|
|-------------|----------|-----------|
|`path`       |`String`  | A reference to the document we want to change for example, `product/1`, the path will contain the collection and document ID so we can find it later. |
|`key`        |`String`  | The value within the document, we will want to change the given value.  Let's say a someone wants to suggest a `name` change to `product/1`  the key would be `name` which leads us to;|
|`value`      |`String`  | The content that is suggested to be changed to.  |
|`createdAt`  |`String`  | Optional, but useful to know when the suggestion was created. |
|`approvedAt` |`DateTime`| A date and time when the suggestion is approved.  We'll explain this a bit later. |
|`committedAt`|`DateTime`| Once the change has been applied we'll mark this with the time the action was applied and finished. Useful to know the change has been firmly committed, or allow us to re-run the job safely.|

## Suggestions Moderation

Depending on your use case you can have a site admin moderation view or a per account moderation view which would be scoped to only the suggestions of the `path` match any of the documents the account has access to. 

I'm not going to cover the per account moderation view in order to keep this writing short.  Feel free to explore this yourself. 

We'll assume a simple administration view where we will fetch all documents in the `suggestions` collection that has a `null` in the  `approvedAt` column. 

Approving the suggestion will set the `approvedAt` value to the current date `new Date()`.

## Handling Suggestions

On Firebase we can set a Firebase Function to listen for changes in FireStore when we update a document.  _In MongoDB or PostgreSQL you can add triggers, and execute a Cloud Function/Lambda_.

We'll aim to do to the following in the Cloud Function;

1. Set the value of the suggested `path` `key` to the `value` given.
1. Set the `committedAt` to the current time.
1. Ensure we do not handle the approval multiple times

Start with a simple function to listen to the `suggestions` documents. 

```javascript
exports.onSuggestionApproved = functions.firestore.document('suggestions/{documentId}')
  .onUpdate((snap, context) => {
    // ... do something when the document is updated
  })
})
```

#### setting the value of the suggestion

We'll use the `path` to get the document reference and can set the `key` and `value`.

```javascript
let ref = db.doc(data.path)
ref.set({
  [data.key]: data.value,
}, { merge: true })
```

#### Finishing the function

The `ref.set` returns a `Promise`, and we will have two promises to wait for. We need to add them to an array and wait for them all to resolve before completing the function. 

Change each instance of `ref.set` to push to a `promises` array

```javascript
let promises = []
promises.push(
  ref.set(/* ... */)
)
```

Return once all the promises are successfully resolved. 

```javascript
return Promise.all(promises)
```

### Putting it all together

```javascript
exports.onSuggestionApproved = functions.firestore.document('suggestions/{documentId}')
  .onUpdate((snap, context) => {
    const before = snap.before.data()
    const data = snap.after.data()
    let promises = []
    /**
     * @todo add approvedBy to ensure the changing document can be modified by a member of the account
     */
    if (!before.approvedAt && data.approvedAt) {
      let ref = db.doc(data.path)
      promises.push(
        ref.set({
        [data.key]: data.value,
        }, { merge: true })
      )
      promises.push(
        snap.after.ref.set({
          committedAt: new Date()
        }, { merge: true })
      )
    }
    return Promise.all(promises)
  })
```

Now we have a simple Cloud Function that listens for changes in a Firestore document and will trigger changes in a corresponding document once the `approvedAt` is defined. 

[![Video Preview](https://j.gifs.com/91j1k8.gif)](https://youtu.be/zbEvk46lVzM)

## Notable Concerns

In the final code, you can see a `@todo`. The power of this function allows any changes to any document.  To avoid people setting themselves as administrators or committing changes to documents they have not authorisation on, we need to add a permission check. 
This is a conditional check that would have to be made before the document is changed. 

## Conclusion

Allowing your community to make changes to database documents allows a whole wealth of crowd-sourced information. 
This example allows the functionality without having to enable or replicate the code for each place we want to implement the feature on. 

Having a suggestions column allows changes to be staged and moderated before they are committed. Also retains a history of the submitted changes for auditing should that be of interest to you. 

If you have any better solutions or questions please comments below.