Firetool

Home / Docs / JS Query

Docs

Run JavaScript queries on Firestore

Write queries in the style of the Firebase Admin SDK: OR filters, cursors, aggregates, transactions, batches, vector search and explain, with the results as a table or JSON. JS Query needs Firetool Pro (or the trial).

Switch a tab to JS Query

  1. Open a collection from the sidebar, as for any query.
  2. At the start of the query bar, choose JS Query (next to Simple).
  3. The filter bar becomes a code box with a starting script for this collection:
// Return a query to see the results as a table.
return db.collection("orders")
  .limit(50)
  .get();

The project button in the query bar works here too: pick another project or database and the same script runs there next time.

JS Query needs Pro. Because scripts can change data, it's also turned off for the Viewer role and in projects or databases marked read-only.

Run a script

  1. Write or paste your script. Tab inserts two spaces.
  2. Choose Run, or press Ctrl+Enter (⌘+Enter on a Mac).
  3. While it runs, Stop ends it.
  4. Under the code you see Finished or Failed, how long it took and how many requests it made, plus anything you printed with console.log().

You can write a full function body with return and await, or a single expression such as db.collection("orders").limit(5).get(). Choose ☆ Save to keep a script in the sidebar's saved queries; a saved script never runs by itself when you open it.

A JS query in Firetool counting orders by status, with the result shown below the code

The Examples menu

Choose Examples ▾ to replace the code with a ready-made script for the open collection:

  • Query with filters
  • Count documents
  • Totals (sum, average)
  • Transaction (read, then write)
  • Vector search
  • Explain a query
  • Update many documents
  • Group by a field
  • Search a collection group
  • Find a user by email
  • List users

The examples use field names such as status and amount: change them to yours before running.

What to return

  • A query snapshot (….get()), a document snapshot, or a list of document snapshots: shown as a table, like a normal query. You can open and edit those documents as usual.
  • Anything else (a number, an object, a list of users, a message): shown as JSON. Timestamps, references and geopoints appear as $timestamp, $ref and $geo, the same notation as the document editor.

Return the result of .get(), not the query itself. An empty list shows "No documents returned".

What you can use

Scripts get these names: db, auth, FieldValue, Timestamp, GeoPoint, Filter, AggregateField, console, sleep(ms), and project and database (where the script runs). admin.firestore() and admin.auth() return db and auth, so Admin SDK snippets need few changes.

WhereMethods
dbcollection(path), collectionGroup(id), doc(path), batch(), getAll(...refs), runTransaction(fn, { maxAttempts, readOnly }), listCollections()
Querieswhere(field, op, value) or where(Filter…), orderBy(field, "asc" | "desc"), limit, limitToLast (needs an orderBy), offset, select(...fields), startAt, startAfter, endAt, endBefore (values or a document snapshot), get(), count(), aggregate({…}), findNearest({…}), explain({ analyze })
Operators== != < <= > >= array-contains array-contains-any in not-in
OR and ANDFilter.where(field, op, value), Filter.or(...), Filter.and(...)
Collectionsdoc(id) (no ID: a new auto ID), add(data), listDocuments()
Documentsget(), set(data, { merge }), create(data), update(data) or update("field", value, …), delete(), collection(id), listCollections()
Snapshotsid, ref, exists, data(), get(field), createTime, updateTime; query snapshots have docs, size, empty, forEach()
Batches and transactionsset, create, update, delete; a batch has commit(); a transaction has get(ref or query) and getAll(...refs)
AggregatesAggregateField.count(), AggregateField.sum(field), AggregateField.average(field)
FieldValueserverTimestamp(), increment(n), arrayUnion(...), arrayRemove(...), delete(), vector([…])
TimestampTimestamp.now(), fromDate(d), fromMillis(ms); toDate(), toMillis(), isEqual()
GeoPointnew GeoPoint(latitude, longitude)
authgetUser(uid), getUserByEmail, getUserByPhoneNumber, listUsers(max, pageToken), createUser, updateUser, setCustomUserClaims, revokeRefreshTokens, deleteUser, deleteUsers, generatePasswordResetLink, generateEmailVerificationLink

findNearest() takes vectorField, queryVector, limit (at most 1,000), distanceMeasure ("EUCLIDEAN", "COSINE" or "DOT_PRODUCT"), and optionally distanceResultField and distanceThreshold. explain() returns metrics (the plan, and with analyze: true the execution stats). On an emulator without an Authentication emulator address, auth isn't available.

Worked examples

OR filter

// Orders that are paid, or worth more than 5,000
return db.collection("orders")
  .where(Filter.or(
    Filter.where("status", "==", "paid"),
    Filter.where("total", ">", 5000)
  ))
  .limit(100)
  .get();

Count and totals without loading the documents

const snap = await db.collection("orders")
  .where("status", "==", "paid")
  .aggregate({
    orders: AggregateField.count(),
    revenue: AggregateField.sum("total"),
    average: AggregateField.average("total")
  })
  .get();
return snap.data();

The next page with a cursor

const q = db.collection("orders").orderBy("createdAt", "desc");
const first = await q.limit(50).get();
if (first.empty) return first;
const last = first.docs[first.docs.length - 1];
return q.startAfter(last).limit(50).get();

Update many documents in batches of 500

const snap = await db.collection("products").where("stock", "<", 5).get();
let batch = db.batch(), n = 0;
for (const doc of snap.docs) {
  batch.update(doc.ref, { lowStock: true, checkedAt: FieldValue.serverTimestamp() });
  if (++n % 500 === 0) { await batch.commit(); batch = db.batch(); }
}
await batch.commit();
return `Marked ${n} products`;

Group by a field

const since = Timestamp.fromDate(new Date("2026-01-01T00:00:00Z"));
const snap = await db.collection("orders").where("createdAt", ">=", since).get();
const counts = {};
snap.forEach(d => {
  const k = d.get("status") ?? "(none)";
  counts[k] = (counts[k] || 0) + 1;
});
return counts;

A user's orders, from their email

const user = await auth.getUserByEmail("someone@example.com");
return db.collection("orders")
  .where("uid", "==", user.uid)
  .orderBy("createdAt", "desc")
  .get();

A query that filters on one field and sorts on another may need a composite index; see Composite indexes.

Limits and safety

  • Time: a script is stopped after 10 minutes.
  • Batches: a batch holds at most 500 writes; commit and start a new one, as above.
  • Requests: a script can make up to 20,000 requests to Firestore and Authentication.
  • Transactions: every read comes before the writes, and a transaction that collides with another save is retried (5 attempts unless you set maxAttempts).
  • Separate process: scripts run in their own process, which has no keys or sign-in, can't read your files (only its own code), can't write files and can't start other programs. Every request goes back through the app.
  • Same checks as the rest of the app: each write passes your role, the project's rules and read-only settings, and lands in the audit log. Each run is logged with its code.
  • Production: on a project marked production, Firetool asks you to type the project ID before the script runs.

Questions

Can I paste code from my Node.js Admin SDK project?

Usually with small changes. Remove the imports and the app setup, use db (or admin.firestore()) and auth, and return what you want to see. Only the methods listed above exist; there's no require and no network access.

Why does my script show JSON instead of a table?

The table is for documents. Return a query snapshot from .get(), a document snapshot, or a list of them. Anything else, including the query itself, a count or an object you built, is shown as JSON.

Can a script delete data by mistake?

It can delete what your role allows, as in the rest of the app. Viewers and read-only projects can't run scripts, production asks for the project ID first, and deletes are recorded in the audit log.