Run Query

Run Query

When working with Firestore, sooner or later you'll come across situations where you need to write simple scripts to read or manipulate your data. The Firebase Admin SDK is the official Google library for server-side interaction with Firebase and makes it easy to interact with Cloud Firestore from JavaScript.

The async run function

When you click the run button, Firefoo will execute the run function and show the returned value in the output panel (Tree/Table/JSON). The Admin SDK is using JavaScript Promises to handle asynchronous operations. The default run function in every JS editor is async, so you can use await in it.
Be aware though: async function invocations that are not awaited will not succeed, because the process is killed after the run function returns (or a returned Promise is resolved).

CORRECT

async function run() { // always await await db.collection("companies").add({ name: "Google" }); }

WRONG

async function run() { // WRONG - DON'T DO THIS !! // the JS process is killed before the data is saved db.collection("companies").add({ name: "Google" }); }

Create Documents

The simplest way to create documents is to await Collection.add. Note that the requests are done sequentially, so for a lot of documents this will take longer than necessary.
async function run() { // add docs sequentially - takes long for a lot of docs await db.collection("companies").add({ name: "Google" }) await db.collection("companies").add({ name: "Apple" }) await db.collection("companies").add({ name: "Microsoft" }) }
To parallelize the requests, you can store the Promises returned from Collection.add in an array and use Promise.all to wait for all requests to complete.
async function run() { // add docs in parallel - faster const promises = []; promises.push( db.collection("companies").add({ name: "Google" }) ); promises.push( db.collection("companies").add({ name: "Apple" }) ); promises.push( db.collection("companies").add({ name: "Microsoft" }) ); return Promise.all(promises); }
Here's a more concise way of doing the same thing, using Array.map.
async function run() { const companies = [{ name: "Google" }, { name: "Apple" }, { name:"Microsoft" }]; const promises = companies.map((data) => db.collection("companies").add(data)); return Promise.all(promises); }

Filter & Order Documents

Use where and orderBy to filter and order your documents. When comparing Timestamps, make sure to convert them to Firestore Timestamp instances, JavaScript Date instances and unix timestamps do not work.
const minDate = new Date("1995-12-17T03:24:00"); const maxDate = new Date("2005-12-18T20:04:00"); const minTimestamp = admin.firestore.Timestamp.fromDate(minDate) const maxTimestamp = admin.firestore.Timestamp.fromDate(maxDate) async function run() { const query = await db.collection("books") .where("publishedDate", ">", minTimestamp) .where("publishedDate", "<", maxTimestamp) .orderBy("publishedDate", "desc") .limit(30) .get(); return query; }

Print Statistics

The Firefoo JS Editor is perfectly suited to execute analytical scripts. Use the lodash library for statistical math functions and print the result to the Log panel using console.log.
async function run() { const query = await db.collection("restaurants") .where("postcode", "==", "6AA") .get(); console.log("number of restaurants in 6AA", query.docs.length); const ratings = query.docs.map((doc) => doc.data().rating); console.log("all ratings", ratings); console.log("min rating", _.min(ratings)); console.log("max rating", _.max(ratings)); console.log("mean rating", _.mean(ratings)); console.log("sum of ratings", _.sum(ratings)); }

Modify Document Data

Let's say you want to change the field name to username in every document of a collection. The easy way is to iterate over every document and update it sequentially, which will be slow if a lot of documents are affected.
async function run() { const query = await db.collection("users").get(); for (const doc of query.docs) { const name = doc.data().name ?? "unnamed"; await doc.ref.update({ username: name, name: admin.firestore.FieldValue.delete(), }); }; }
A faster way is to send the updates out in parallel. Make sure to use Promise.all , so that the process is not killed until all update operations are completed.
async function run() { const query = await db.collection("users").get(); const promises = query.docs.map((doc) => { const fieldUpdates = { username: doc.data().name ?? "unnamed", name: admin.firestore.FieldValue.delete(), } return doc.ref.update(fieldUpdates); }); return Promise.all(promises); }

Batches

To minimize the amount of network requests, you can use batched writes of up to 500 operations. Batches can contain multiple write operations (set, update, delete), but cannot read data. The lodash chunk function comes in handy to create chunks of the right size.
async function run() { const query = await db.collection("users").get(); const docChunks = _.chunk(query.docs, 500); for (const docChunk of docChunks) { const batch = db.batch(); for (const doc of docChunk) { const fieldUpdates = { username: doc.data().name ?? "unnamed", name: admin.firestore.FieldValue.delete(), } batch.update(doc.ref, fieldUpdates); } console.log(`committing chunk of ${docChunk.length} updates`) await batch.commit(); } }

Joins

Firestore does not support joining collections in the backend. For small collections, you can download and join them locally. Let's say you have employees (name, companyId) and companies (name, revenue). To list every employee with their company name whose company has more than 10k in revenue:
async function run() { const employeePromise = db.collection("employees").get(); const companyPromise = db.collection("companies").where("revenue", ">", 10000).get(); const [employeeQuery, companyQuery] = await Promise.all([employeePromise, companyPromise]); const companyDocsById = _.keyBy(companyQuery.docs, (doc) => doc.id); for (const employeeDoc of employeeQuery.docs) { const employee = employeeDoc.data(); const companyDoc = companyDocsById[employee.companyId]; if (companyDoc != null) { const company = companyDoc.data(); console.log(`${employee.name} works at ${company.name} (${company.revenue} revenue)`); } } }

Global Variables

There are three global variables that you can use in the script:
  • admin The admin variable holds a reference to the Admin SDK entry point, already initialized with the current Firebase project. If you need to delete a field or set a server timestamp, you can access the FieldValue object from it: admin.firestore.FieldValue.delete()
  • db The db variable is a convenience shortcut to admin.firestore(), which contains essential functions likedb.collection(collectionPath) and db.doc(docPath).
  • _ (lodash) The lodash library with useful functions.
    • _.chunk(docs, 10) Split the docs array into chunks, the result is an array of arrays, where each inner array (other than the last one) will have a length of 10. Useful for batch operations.
    • _.groupBy(docs, (d) => d.data().type) Group your data. Similar to the GROUP BY statement in SQL.
    • _.sum(values), _.mean(values), _.max(values) Calculate the sum, mean or max of an array of numbers.

Troubleshooting

I get an error about indexes

Just follow the link to the Firebase Console and click the button to create your compound index! Creating the index will take a few minutes even for empty databases! There's a limit of 200 indexes per Firestore database, which should be more than enough for most use cases.

FAQ

What's the difference between the Admin SDK and the Android/iOS/Web client libraries?

The api of the Admin SDK is very similar to those of client libraries for the web and mobile, with some notable differences: The Admin SDK bypasses all Firestore security rules, so you don't have to worry about that. It also contains additional functions to list documents and (sub)collections with CollectionReference.listDocuments() and DocumentReference.listCollections().

Can I import NPM packages?

Not yet, unfortunately. But stay tuned, it's on our roadmap!