Let’s say you’re counting the number of cities in a country, so you register an onDocumentCreated
trigger for:
/countries/{countryId}/cities/{cityId}
In the trigger, you’ll want to increase the cityCount
in the Country document at /countries/{countryId}
. How should this be done?
The documentation states:
Collections and documents are created implicitly in Cloud Firestore.
Simply assign data to a document within a collection.
If either the collection or document does not exist, Cloud Firestore creates it.
My approach of running a transaction to update the count failed, because the parent document doesn’t exist at the moment the first city in that country was created.
const {onDocumentCreated} = require("firebase-functions/v2/firestore");
const {initializeApp, getApps} = require("firebase-admin/app");
const {getFirestore, DocumentReference} = require("firebase-admin/firestore");
if (!getApps().length) initializeApp();
const db = getFirestore();
exports.firestoreEventHander = onDocumentCreated(
"countries/{countryId}/cities/{cityId}",
async (event) => {
const snapshot = event.data;
console.log("snapshot exists:", snapshot.exists); // true
const countryRef = snapshot.ref.parent.parent;
console.log("country path:", countryRef.path); // countries/USA
// Access snapshot through the db
const cityDoc = await db.doc(`countries/${countryId}/cities/${cityId}`).get();
console.log("cityDoc.exists:", cityDoc.exists); // true
// Check if the countryRef document exists
const countryDoc = await countryRef.get();
console.log("countryDoc:", countryDoc.data()); // ⚠️ undefined ⚠️
console.log("countryDoc.exists:", countryDoc.exists); // ⚠️ false ⚠️
// Try to access the country via db.doc()
const countryDoc2 = await db.doc(`countries/${countryId}`).get();
console.log("countryDoc2:", countryDoc2.data()); // ⚠️ undefined ⚠️
console.log("countryDoc2.exists:", countryDoc2.exists); // ⚠️ false ⚠️
},
);
The Trigger a function when a new document is created example doesn’t mention anything about the parent document of newly created nested documents not existing yet.
2
Answers
Apparently, and counter-intuitively to me in Firestore, "child documents are perfectly capable of existing with or without a parent document". While it is documented that after deleting a document, its subcollections are not automatically deleted (that's why
recursiveDelete
exists), it's not documented that creating a document may not create its parent.With that in mind, I've worked around this issue by replacing the
update
call with aset
with{ merge: true }
. This will create the document if it doesn't exist.If you want to update a document inside a transaction, using the Firebase Admin SDK when you don’t already know if the document exists or not, there is only one main step to perform, with two sub-steps:
This is all easy to achieve using the transaction API. Any changes you make with the API will occur all at once or not at all. It doesn’t matter if the document previously exists or not.
This procedure works regardless of the parent/child relationship of the documents. As you can imagine, child documents are perfectly capable of existing with or without a parent document. There is only a path string needed in order to read and write a document.