Run JavaScript Query
Run JavaScript 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. Firefoo has a JavaScript Query editor to make writing scripts fast and seamless. Click a collection in the sidebar, then switch to the JS Query mode. In your script you can use the Firebase Admin SDK, the official Google library for server-side interaction with Firebase. The JavaScript will run in a Node.JS runtime on your local machine.
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 when 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" });
}
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 await 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 await Promise.all(promises);
}
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;
}
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));
}
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 await Promise.all(promises);
}
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();
}
}
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)`);
}
}
}
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)
anddb.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.
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.
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().
Not yet, unfortunately. But stay tuned, it's on our roadmap!