SQL for your arrays and your Firestore and Realtime Database reads. Write a loop, or a read from the database, as select … where … join … group by … order by … limit.
SELECT city, COUNT(*) AS n, SUM(price) AS total
FROM orders JOIN customers ON orders.customerId = customers.id
WHERE customers.tier = 'gold' AND orders.status IN ('open', 'paid')
GROUP BY city ORDER BY total DESC LIMIT 5ListQuery.from(await db.orders.whereIn('status', ['open', 'paid']).get()) // Firestore runs the IN
.innerJoin(await db.customers.withKey('id').where('tier', '==', 'gold').get(), 'customerId', 'id')
.select({ city: row => row.right.contact.city, price: row => row.left.price })
.groupBy('city')
.aggregate(a => ({ n: a.count(), total: a.sum('price') }))
.orderBy('total', 'desc')
.limit(5)
.toList();The same description runs on an array you hold (from), reads a Firestore or Realtime Database list once (get), and follows it live (listen). The database does what it can, the rest finishes locally, and the answer is the same either way.
Built for frontend apps on the Firebase web SDK, and it runs unchanged in Node, so the same query code can be shared with Cloud Functions. The core has no Angular, React, Vue, Svelte, RxJS, browser global, Node API, or Firebase SDK import. It works under any framework or none, and stays safe to import while a page is server-rendered. Firebase sits behind an injectable transport, so everything is testable without a Firebase project.
pnpm add wherelive # or: npm install whereliveCurrent release: 0.1.0 on npm. firebase (10 or later) and rxjs (7 or later) are optional peer dependencies. Install firebase to use get and listen through wherelive/firebase, and rxjs only for wherelive/rxjs. Plain ListQuery over arrays needs neither.
1. Query an array. No Firebase needed.
import { ListQuery } from 'wherelive';
ListQuery.from(orders)
.where('status', '==', 'open')
.where('customer.address.city', '==', 'Lyon')
.orderBy('placedAt', 'desc')
.limit(10)
.select('id', 'total')
.toList();Everything is typed on the chain. where('customer.nam', …), select('nam') and sum('name') do not compile.
2. Read Firestore or Realtime Database. Describe your paths once, then use the same vocabulary. What the database can do it does, and the rest finishes locally.
import { leaf, schema, firestoreBackend } from 'wherelive';
import { firebaseFirestoreTransport } from 'wherelive/firebase';
import { getFirestore } from 'firebase/firestore';
const db = schema(
{ orders: (id: string) => leaf<Order>() },
firestoreBackend(firebaseFirestoreTransport(getFirestore())),
);
const rows = await db.orders
.withKey('$key')
.where('status', '==', 'open')
.orderBy('total', 'desc')
.limit(20)
.select({ id: '$key', total: 'total' })
.get('loadOpenOrders');3. Listen live, to only what you selected. listen returns stop, so it fits any framework's mount and teardown. Callers with the same query share one connection.
const stop = db.orders
.where('status', '==', 'open')
.select('status', 'total')
.listen(change => render(change), 'watchOpenOrders'); // { key, attribute, value }
// later: stop();| Guide | What is in it |
|---|---|
| SQL to wherelive | Every SQL clause next to its wherelive call |
| Query arrays | ListQuery, loops as queries, group, aggregate, join, union |
| Describe the paths once | The schema, pathOf, id cleaning, decoding values |
| Read Firestore and Realtime Database | What runs on the server, what runs here, and why the answer is the same |
| explain() and indexes | What a query sends, and the indexes Firebase needs |
| Listen live | listen, shared connections, errors, RxJS, components, Node |
| Testing your own code | In-memory transports for tests |
| Speed | Benchmarks against hand-written loops |
| Developing wherelive | Tooling and the emulator tests |
wherelive what you use: ListQuery, schema, leaf, the two backends, the errors, and 28 names in all
wherelive/firebase transports on the real Firebase SDK needs `firebase`
wherelive/rxjs observe(): a listen as an Observable needs `rxjs`
wherelive/testing in-memory transports for tests
wherelive/transport only to write your own transport or backend: the shapes they speak, 28 names
These wait until a caller needs them: whereEqualUnless, whereEqualOrEmpty, intersect and except, some(callback) (use .where(fn).some()), a Firestore query built from a DocumentSnapshot, a QueryState snapshot, and the React/Vue/Angular/Svelte adapters.
Issues, questions and pull requests are welcome. Read CONTRIBUTING.md to set up and see what is most wanted (an Admin SDK transport is the biggest gap). Ask a question or share how you use it in Discussions. Everyone taking part follows the Code of Conduct. For a security problem, see SECURITY.md. Changes are listed in CHANGELOG.md.
If wherelive saves your team time, starring the repo helps others find it.