First, I want to add a document to Firestore:
await setDoc(product, docData, {
merge: true
});
Once I'm 100% sure that the document has been written successfully, I want to add/update another document:
const products_array_snap = await getDoc(products_array);
if (products_array_snap.exists()) {
await updateDoc(products_array, {
products_array: arrayUnion(productSKU)
});
console.log("Product array exists:", products_array_snap.data());
} else {
await setDoc(products_array, {
products_array: arrayUnion(productSKU),
products_default: productSKU
});
console.log("No product array!");
}
How can this code be rewritten to handle errors and make sure that both documents are set up correctly together?
CodePudding user response:
Because your operation relies on the state of the server (thanks to products_default
), you'll need to wrap the whole thing in a transaction. A transaction will only make changes to the database in an all-or-nothing (atomic) fashion, so you can guarantee that if it resolves, both your product document and the product array will be updated properly.
These are the variables the following blocks work with:
import { runTransaction, getFirestore, doc, collection } from "firebase/firestore";
const db = getFirestore();
const products_array_ref = doc(db, 'docIds/products');
const product_ref = doc(collection(db, 'products'));
const product_id = product_ref.id;
const product_data = { /* ... */ };
To use runTransaction
, you pass in the Firestore instance you are using along with an "update function" that uses a Transaction
to queue changes to your database. It's important to remember that the update function may be called more than once if the data on the server side changes while you are processing it (e.g. another product added/removed).
In this function, you must first pull down all the dependencies that your transaction relies on. If needing to read multiple documents at once, use Promise.all()
to grab them in parallel. Once you have all the dependencies, you can start queuing changes to be committed to the server. If you return a value from the update function, it will be passed out of runTransaction
when it finally resolves. You can use this to track how many tries it took, if you aborted the transaction voluntarily, if the original document existed or not, etc. If the transaction failed to be written due to contention or some other issue, runTransaction
will reject its Promise with the appropriate error.
For your use case, you can write your logic like so:
const products_array_existed = await runTransaction(db, async (transaction) => {
// dependencies
const products_array_snap = await transaction.get(products_array_ref);
// queue changes
transaction.set(product_ref, product_data, { merge: true });
if (products_array_snap.exists()) {
transaction.update(
products_array_ref,
{ products_array: arrayUnion(product_id) }
);
} else {
transaction.set(
products_array_ref,
{
products_array: arrayUnion(product_id),
products_default: product_id
},
{ merge: true }
);
}
// set return value
return products_array_snap.exists();
});
console.log(`Products array ${products_array_existed ? "already existed" : "was created"}.`);
or
const products_array_existed = await runTransaction(db, async (transaction) => {
// dependencies
const products_array_snap = await transaction.get(products_array_ref);
const products_array_change = {
products_array: arrayUnion(product_id),
...(products_array_snap.exists() ? {} : { products_default: product_id })
}
// queue changes
transaction.set(product_ref, product_data, { merge: true });
transaction.set(products_array_ref, products_array_change, { merge: true });
// set return value
return products_array_snap.exists();
});
console.log(`Products array ${products_array_existed ? "already existed" : "was created"}.`);