@sofiakb/fireblaze-node-ts
v1.2.0-dev.1
Published
A repository layer for Firestore, built on firebase-admin.
Readme
About The Library
@sofiakb/fireblaze-node-ts wraps the Firestore Admin SDK in a small repository/active-record-style
class (FirestoreRepository<T>) so you can model a collection as a class instead of calling
firestore.collection(...) by hand everywhere: casting documents to your own model, a chainable query
builder, soft deletes, pagination, unique-key enforcement, and batched bulk operations.
Built With
Prerequisites
- A Firebase Admin app already initialized (
firebase-admin), or aFirestoreinstance you pass in yourself. - Node.js and TypeScript.
Installation
npm install --save @sofiakb/fireblaze-node-tsUsage
Define a model and a repository
import { FirestoreRepository } from "@sofiakb/fireblaze-node-ts";
class User {
id!: string;
name!: string;
email!: string;
createdAt: any;
updatedAt: any;
constructor(attributes: any = {}) {
Object.assign(this, attributes);
}
}
class UserRepository extends FirestoreRepository<User> {
constructor() {
super({ collectionName: "users", model: User });
}
}By default FirestoreRepository resolves the Firestore instance via getFirestore() (the default
Firebase Admin app). Pass firestore in the constructor attributes to use a specific instance (e.g. a
named app, or the Firestore emulator).
CRUD
const users = new UserRepository();
const user = await users.store({ name: "Alice", email: "[email protected]" });
const found = await users.find(user.id);
await users.update(user.id, { name: "Alice B." });
await users.delete(user.id);Querying
import { Filter } from "@google-cloud/firestore";
const adults = await users
.where(Filter.where("age", ">=", 18))
.orderBy("age", "desc")
.limit(20)
.get();
const first = await users.where(Filter.where("email", "==", "[email protected]")).first();Pagination
const page = await users.paginate(2, 20); // page, per-page
// page.data, page.page, page.per, page.total, page.latest, page.prev, page.nextSoft deletes
class UserRepository extends FirestoreRepository<User> {
constructor() {
super({ collectionName: "users", model: User });
this.softDeletes = true;
}
}
await users.softDelete(user.id);
await users.find(user.id); // null by default
await users.find(user.id, { withSoftDelete: true }); // returns the documentUnique constraints
import { UniqueConstraintException } from "@sofiakb/fireblaze-node-ts";
try {
await users.storeUnique({ name: "Alice", email: "[email protected]" }, ["email"]);
} catch (e) {
if (e instanceof UniqueConstraintException) {
// an existing document already has this email
}
}Bulk operations
await users.storeMultiple([{ name: "A" }, { name: "B" }]);
await users.updateMultiple(["id1", "id2"], { active: true });
await users.deleteMultiple(["id1", "id2"]);
await users.whereIn("id", ["id1", "id2", "id3"]);
await users.whereNotIn("id", ["id1"]);Related-data appends
class UserRepository extends FirestoreRepository<User> {
async withPostsCount(data: any): Promise<number> {
// `this` is the repository instance here
return (await postsRepository.where(Filter.where("authorId", "==", data.id)).get()).length;
}
}
const user = await users.with("postsCount").find(id);
// user.postsCount is now populatedAPI overview
| Method | Description |
| --- | --- |
| store(data) / storeUnique(data, uniqueKeys) / storeMultiple(values) | Create one or more documents |
| find(id, options?) / doc(id, cast?, options?) / findOneById(id) | Fetch a single document |
| update(id, data, force?) / updateArray(id, key, ...values) / updateMultiple(ids, data) | Update documents |
| delete(id) / deleteMultiple(values) / softDelete(id) / truncate() | Remove documents |
| where(filter) / orderBy / limit / limitToLast / startAt / startAfter / endAt / endBefore / first() / get(cast?) | Chainable query builder |
| whereIn(column, values) / whereNotIn(column, values) | in / not-in queries, automatically chunked past Firestore's 10-value limit |
| count(all?) / countQuery() / paginate(page, limit) | Counting and pagination |
| copy(destCollectionName) / fields.add(fields) / fields.delete(fields) | Collection-level maintenance helpers |
| search(column, query) | Best-effort case-insensitive prefix search — see Known limitations |
| with(key) / appends | Populate computed/related fields via with<Key>() methods on a repository subclass |
Known limitations
search()builds a Firestore range query intended to approximate a case-insensitive substring search. It reliably matches prefixes, but for values that only contain the query as a substring further in the string, whether they're returned currently depends on the case of the value's first character (e.g. searching"an"finds"Banana"but not"banana"). This is a known, documented limitation — Firestore has no native case-insensitive substring index, and a fully correct fix would mean either scanning the whole collection on every call or requiring a normalized field maintained by the caller. Neither is a drop-in fix, so it hasn't been changed. Seetests/integration/search.integration.test.tsfor the exact behavior this locks in.
Development
npm install
npm run build # tsc + tsc-alias -> lib/
npm run lint # prettier + eslint --fix
npm test # unit tests (Jest)
npm run test:emulator # integration tests against the Firestore emulator (requires the Firebase CLI + a JRE)Unit tests (src/**/__tests__/*.test.ts) don't need any external service. Integration tests
(tests/integration/*.integration.test.ts) run against a local Firestore emulator started by
firebase emulators:exec (config in firebase.json/firestore.rules/.firebaserc).
Roadmap
See the open issues for a list of proposed features (and known issues).
License
Distributed under the MIT License. See LICENSE for more information.
