Getting error: `Every document read in a transacti

2019-06-07 04:58发布

guys!

i am developing an app similar to https://airtasker.com where users outsource tasks. the taskers would bid to the tasks, and wait for the user to approve their bids.

these are the involved collections:

  • tasks
  • transactions
  • bids

basically, this function should:

check if a transaction exists with the given taskId. a transaction is added if the user starts to approve bids. i allow multiple taskers to complete the task.

if a transaction doesn't exist, it should add a new one, mark the status ongoing if it reaches the required manpower (otherwise pending), and update the bids collection to mark the bid accepted.

if a transaction exists, it should check if the current approved list from the transactions collection is equal to the manpower

if it hasn't reached the quota manpower yet, push a new tasker and access the bids collection to mark the bid accepted.

if after the last condition, the approved list already reached the quota manpower, mark the task close, and change the status of the transaction as ongoing

but i keep getting this error:

Uncaught (in promise) Error: Every document read in a transaction must also be written. at Transaction.commit (transaction.js:128) at eval (sync_engine.js:244)

here's my code:

const acceptOffer = async (taskerId, taskId, bidId, offer) => {
  let bulk
  try {
    const taskRef = db.collection('tasks').doc(taskId)
    const transRef = db.collection('transactions').doc(taskId)
    const bidRef = db.collection('bids').doc(bidId)
    const fees = solveFees(offer)

    bulk = await db
      .runTransaction(async t => {
        const transdoc = await t.get(transRef)
        const taskdoc = await t.get(taskRef)
        const manpower = await taskdoc.get('manpower')

        let status = 'pending'
        if (manpower === 1) {
          status = 'ongoing'
        }

        if (!transdoc.exists) {
          t.set(transRef, {
            taskId,
            status, // pending, ongoing, completed
            approved: [
              { taskerId, ...fees }
            ]
          })

          t.update(bidRef, {
            accepted: true
          })
        } else {
          const approved = await transdoc.get('approved')
          if (manpower < approved.length) {
            approved.push({ taskerId, ...fees })
            t.update(transRef, { approved })
            t.update(bidRef, { accepted: true })

            if (manpower === approved.length) {
              t.update(taskRef, { open: false })
              t.update(transRef, { status: 'ongoing' })
            }
          }
        }
      })
  } catch (e) {
    bulk = e
    console.log('nag error', e)
    throw e
  }

  if (bulk.success) {
    swal('Offer accepted!', '', 'success')
  } else {
    swal('Oh, no!',
      'This task might already be approved',
      'error'
    )
  }
}

i have been stuck here since i don't understand where the transaction failed. any help is very much appreciated.

thank you!

2条回答
啃猪蹄的小仙女
2楼-- · 2019-06-07 05:35

to those who are having the same problem, here is my (hackish) solution:

for every condition,

add a document write (could be a set() update() or delete()) that corresponds to each of the document reads which in my code: the use of get()s.

and return a Promise

here's the updated code:

// a transaction is added if the user starts to approve offers
// this function allows multiple taskers
const acceptOffer = async (taskerId, taskId, bidId, offer) => {
  let bulk
  try {
    const taskRef = db.collection('tasks').doc(taskId)
    const transRef = db.collection('transactions').doc(taskId)
    const bidRef = db.collection('bids').doc(bidId)

    const fees = solveFees(offer)

    bulk = await db
      .runTransaction(async t => {
        const transdoc = await t.get(transRef)
        const taskdoc = await t.get(taskRef)
        const manpower = await taskdoc.get('manpower')

        // check if a transaction exists with the given taskId
        // if it doesn't, then the task doesn't have
        // any approved bidders yet
        if (!transdoc.exists) {
          // check if there is only one manpower required for the task
          // mark the status of the transaction 'ongoing' if so
          const status = manpower === 1
            ? 'ongoing' : 'pending'

          // add a transaction with the approved tasker
          t.set(transRef, {
            taskId,
            status, // pending, ongoing, completed
            approved: [
              { taskerId, ...fees }
            ]
          })

          // mark the bid 'accepted'
          t.update(bidRef, {
            accepted: true
          })

          // hackish (to prevent firestore transaction errors)
          t.update(taskRef, {})

          return Promise.resolve(true)
        } else { // if a transaction exists with the given taskId
          const approved = await transdoc.get('approved')

          // check if the current approved list from
          // the transactions collection hasn't
          // reached the manpower quota yet
          if (approved.length < manpower) {
            // push new approved bid of the tasker
            approved.push({ taskerId, ...fees })
            t.update(transRef, { approved })

            t.update(bidRef, { accepted: true }) // mark the bid 'accepted'
            t.update(taskRef, {}) // hackish

            // if, after pushing a new transaction,
            // the approved list reached the manpower quota
            if (approved.length === manpower) {
              t.update(taskRef, { open: false }) // mark the task 'close'
              t.update(transRef, { status: 'ongoing' }) // mark the transaction 'ongoing'
              t.update(bidRef, {}) // hackish
            }
            return Promise.resolve(true)
          }
          return Promise.reject(new Error('Task closed!'))
        }
      })
  } catch (e) {
    swal('Oh, no!',
      'This task might already be closed',
      'error'
    )
    throw e
  }

  if (bulk) {
    swal('Offer accepted!', '', 'success')
  }
}
查看更多
可以哭但决不认输i
3楼-- · 2019-06-07 05:39

I ran into the same issue. As long as google will not be able to sent validation errors with better errors than just that the client was not allowed to write the data (security rules). I prefer to handle it on client site. So I use transactions for example to validate that a referenced doc is still available when I write data. (for example I have write an order document that references to a customer and want be sure that the customer still exists.) So I have to read it but actually there is no need to write it.

I came up with something close to nrions solution but tried to have a more general approach for it so I wrote a wrapper for runTransaction. Of cause it is not the cleanest way to do it but maybe it is useful for others.

// Transaction neads to write all docs read be transaction.get().
// To work around this we we call an update with {} for each document requested by transaction.get() before writing any data
export function runTransaction(updateFunction) {
  return db.runTransaction(transaction => {
    const docRefsRequested = [];
    let didSetRequestedDocs = false;

    function setEachRequestedDoc() {
      if (didSetRequestedDocs) {
        return;
      }
      didSetRequestedDocs = true;
      docRefsRequested.forEach(({ exists, ref }) => {
        if (exists) {
          transaction.update(ref, {});
        } else {
          transaction.delete(ref);
        }
      });
    }

    const transactionWrapper = {
      get: function(documentRef) {
        return transaction.get(ref).then(snapshot => {
          const { exists } = snapshot;
          docRefsRequested.push({ ref, exists });
          return Promise.resolve(snapshot);
        });
      },
      set: function(documentRef, data) {
        setEachRequestedDoc();
        return transaction.set(documentRef, data);
      },
      update: function(documentRef, data) {
        setEachRequestedDoc();
        return transaction.update(documentRef, data);
      },
      delete: function(documentRef) {
        setEachRequestedDoc();
        return transaction.delete(documentRef);
      },
    };

    return updateFunction(transactionWrapper).then(resolveValue => {
      setEachRequestedDoc();
      return Promise.resolve(resolveValue);
    });
  });
}
查看更多
登录 后发表回答