Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Firebase Functions onUpdate circular problem

I've this situation with a circular function, having trouble finding a solution.

Have a collection where I have a flag that tells if the data has changed. Also want to log the changes.

export async function landWrite(change, context) {

  const newDocument = change.after.exists ? change.after.data() : null
  const oldDocument = change.before.data()

  const log = {
    time: FieldValue.serverTimestamp(),
    oldDocument: oldDocument,
    newDocument: newDocument
  }

  const landid = change.after.id
  const batch = db.batch()

  const updated = newDocument && newDocument.updated === oldDocument.updated

  if (!updated) {
    const landRef = db.collection('land').doc(landid)
    batch.update(landRef, {'updated': true })
  }
  const logRef = db.collection('land').doc(landid).collection('logs').doc()
  batch.set(logRef, log)

  return batch.commit()
  .then(success => {
    return true
  })
  .catch(error => {
    return error
  })

}

The problem is that this writes the log twice when the UPDATED flag is false. But also cannot put the log write in the ELSE statement because the flag can already be UPDATED and a new document update be made so a new log has to be written.

Trigger:

import * as landFunctions from './lands/index'
export const landWrite = functions.firestore
.document('land/{land}')
.onWrite((change, context) => {
  return landFunctions.landWrite(change, context)
})
like image 346
InfoStatus Avatar asked May 29 '19 08:05

InfoStatus


2 Answers

If I understand correctly, the problem here is that the updated flag does not specify which event the update is in response to (as you can't really do this with a boolean). In other words - you may have multiple simultaneous "first-stage" writes to lands, and need a way to disambiguate them.

Here are a few possible options that I would try - from (IMHO) worst to best:

  • The first option is not very elegant to implement
  • The first and second options both result in your function being called twice.
  • The third option means that your function is only called once, however you must maintain a separate parallel document/collection alongside lands.

Option 1

Save some sort of unique identifier in the updated field (e.g. a hash of the stringified JSON event - e.g. hash(JSON.stringify(oldDocument)), or a custom event ID [if you have one]).

Option 2

Try checking the updateMask property of the incoming event, and discard any write events that only affect that property.

Option 3

Store your update status in a different document path/collection (e.g. a landUpdates collection at the same level as your lands collection), and configure your Cloud Function to not trigger on that path. (If you need to, you can always create a second Cloud Function that does trigger on the landUpdates path and add either the same logic or different logic to it.)

Hope this helps!

like image 151
Ace Nassri Avatar answered Nov 03 '22 08:11

Ace Nassri


The main problem here is the inability of differentiating changes that are made by this server function or by a client. Whenever you are in this situation, you should try to explicitly differentiate between them. You can even consider having an extra field like fromServer: true that goes with server's updates and helps the server ignore the related trigger. Having said that, I think I have identified the issue and provided a clear solution below.

This line is misleading:

  const updated = newDocument && newDocument.updated === oldDocument.updated

It should be named:

  const updateStatusDidNotChange = newDocument && newDocument.updated === oldDocument.updated

I understand that you want the updated flag to be managed by this function, not the client. Let me know if this is not the case.

Therefore, the update field is only changed in this function. Since you want to log only changes made outside of this function, you want to log only when updated did not change.

Here's my attempt at fixing your code in this light:

export async function landWrite(change, context) {

  const newDocument = change.after.exists ? change.after.data() : null
  const oldDocument = change.before.data()

  const updateStatusDidNotChange = newDocument && newDocument.updated === oldDocument.updated

  if (!updateStatusDidNotChange) return true; //this was a change made by me, ignore

  const batch = db.batch()

  if (!oldDocument.updated) {
    const landid = change.after.id
    const landRef = db.collection('land').doc(landid)
    batch.update(landRef, {'updated': true })
  }

  const log = {
    time: FieldValue.serverTimestamp(),
    oldDocument: oldDocument,
    newDocument: newDocument
  }

  const logRef = db.collection('land').doc(landid).collection('logs').doc()
  batch.set(logRef, log)

  return batch.commit()
  .then(success => {
    return true
  })
  .catch(error => {
    return error
  })

}

Edit

I had the exact problem and I had to differentiate changes by the server and the client, and ignore the ones that were from the server. I hope you give my suggestion a try.

like image 3
Gazihan Alankus Avatar answered Nov 03 '22 08:11

Gazihan Alankus