Mulan
Mulan

Reputation: 135415

How to update firestore collection based on other docs?

I am building an order form that limits how many items you can order based on the stock of the item. I have a menu collection which has items

// menu 

{ id: "lasagna", name: "Lasagna", price: 10, stock: 15 }
{ id: "carrot-soup", name: "Carrot Soup", price: 10, stock: 15 }
{ id: "chicken-pot-pie", name: "Chicken Pot Pie", price: 10, stock: 15 }

And an orders collection

// orders

{ id: <auto>, name: "Sarah", cart: {lasagna: 1, carrot-soup: 3}, ... }
{ id: <auto>, name: "Wendy", cart: {chicken-pot-pie: 2, carrot-soup: 1}, ... }
{ id: <auto>, name: "Linda", cart: {lasagna: 3}, ... }

4 carrot-soup has been ordered so the stock should be updated

// updated stock

{ id: "carrot-soup", name: "Carrot Soup", stock: 11 }

Orders are inserted from my Form component

function Form(props) {   
  // ... 

  // send order to firestore
  const onSubmit = async _event => {
    try {
      const order = { cart, name, email, phone, sms }
      dispatch({ action: "order-add" })
      const id = await addDocument(store, "orders", order)
      dispatch({ action: "order-add-success", payload: { ...order, id } })
    }
    catch (err) {
      dispatch({ action: "order-add-error", payload: err })
    }
  }
  return <form>...</form>
}

This is my database addDocument function

import { addDoc, collection, serverTimeStamp } from "firebase/firestore"

async function addDocument(store, coll, data) {
  const docRef = await addDoc(collection(store, coll), { ...data, timestamp: serverTimestamp() })
  return docRef.id
}

How should I decrement the stock field in my menu collection? Ideally the client should have only read access to menu but to update the stock the client would need write access.

Another possibility is to have the client query the orders, sum the items, and subtract them from the read-only menu. But giving the client read access to other people's orders seems wrong too.

I am new to firestore and don't see a good way to design this.

Upvotes: 0

Views: 543

Answers (2)

Mulan
Mulan

Reputation: 135415

Here is a solution based on Tarik Huber's advice.

First I include functions and admin

const functions = require("firebase-functions")
const admin = require("firebase-admin")
admin.initializeApp()

Then I create increment and decrement helpers

const menuRef = admin.firestore().collection("menu")

const increment = ([ id, n ]) =>
  menuRef.doc(id).update({
    stock: admin.firestore.FieldValue.increment(n)
  })

const decrement = ([ id, n ]) =>
  increment([ id, n * -1 ])

Here is the onCreate and onDelete hooks

exports.updateStockOnCreate =
  functions
    .firestore
    .document("orders/{orderid}")
    .onCreate(snap => Promise.all(Object.entries(snap.get("cart") ?? {}).map(decrement)))

exports.updateStockOnDelete =
  functions
    .firestore
    .document("orders/{orderid}")
    .onDelete(snap => Promise.all(Object.entries(snap.get("cart") ?? {}).map(increment)))

To handle onUpdate I compare the cart before and after using a diff helper

exports.updateStockOnUpdate =
  functions
    .firestore
    .document("orders/{orderid}")
    .onUpdate(snap => Promise.all(diff(snap.before.get("cart"), snap.after.get("cart")).map(increment)))

Here is the diff helper

function diff (before = {}, after = {}) {
  const changes = []
  const keys = new Set(Object.keys(before).concat(Object.keys(after)))
  for (const k of keys) {
    const delta = (before[k] ?? 0) - (after[k] ?? 0)
    if (delta !== 0)
      changes.push([k, delta])
  }
  return changes
}

Upvotes: 0

Tarik Huber
Tarik Huber

Reputation: 7418

You should deffinitely use a cloud function to update the stock. Create a function onCreate and onDelete functions trigger. If users can change data you would also need to onWrite function trigger.

Depending on the amount of data you have you woould need to create a custom queue system to update the stock. Belive me! It took me almost 2 years to figure out to solve this. I have even spoken with the Firebase engeeners at the last Firebase Summit in Madrid.

Usualy you would use a transaction to update the state. I would recommend you to do so if you don't have to much data to store.

In my case the amount of data was so large that those transactions would randomly fail so the stock wasn't correct at all. You can see my StackOverflow answer here. The first time I tought I had an answer. You know it took me years to solve this because I asked the same question on a Firebase Summit in Amsterdam. I asked one of the Engeeners who worked on the Realtime Database before they went to Google.

There is a solution to store the stock in chunks but even that would cause random errors with our data. Each time we improved our solution the random errors reduced but still remained.

The solution we are still using is to have a custom queue and work each change one by one. The downside of this is that it takes some time to calculate a lot of data changes but it is 100% acurate.

Just in case we still have a "recalculator" who recalculates one day again and checks if everything worked as it should.

Sorry for the long aswer. For me it looks like you are building a similar system like we have. If you plan to create a warehouse management system like we did I would rather point you to the right direction.

In the end it depends on the amount of data you have and how often or fast you change it.

Upvotes: 1

Related Questions