firestore-model-management
v0.1.5
Published
Hybrid TypeScript models + generators for Firestore (CASL + rules codegen)
Maintainers
Readme
Firestore Model Management
This package provides a lightweight hybrid model system for Firestore with:
BaseModelfor defining data models and serialization (toFirestore/fromFirestore).FirestoreManagerfor performing Firestore reads/writes and resolving foreign keys.metaToCASLRulesutility to generate CASL rules from modelMETA.permissions.FirestoreRulesGeneratorto compileMETA.permissionsinto Firestore security rule snippets.scripts/generate-permissions.tsCLI to generategenerated/casl-rules.tsandfirestore/generated_permissions.rules.
Quickstart
- Define your model by extending
BaseModelinmodels/YourModel.tsand setstatic METAincludingpermissions. - Register model classes with
FirestoreManagerin your app bootstrap:
import { FirestoreManager } from './models/firestoreManager'
import { User } from './models/abstract'
const fm = new FirestoreManager(db)
fm.registerModel('users', User)- To regenerate CASL + Firestore rules after editing
META:
node scripts/generate-permissions.js
# or with ts-node
node -r ts-node/register scripts/generate-permissions.tsThe generated files will be:
generated/casl-rules.ts— importable CASL rules for frontend/backend.firestore/generated_permissions.rules— snippet to merge into your Firestore rules.
FirestoreManager usage
Register models (collection ↔ class)
FirestoreManager keeps a registry of collection names to model classes, and the reverse mapping (class → collection) for convenience.
import { FirestoreManager } from './models/firestoreManager'
const manager = new FirestoreManager(db)
manager.registerModel('users', UserModel)
manager.registerModel('posts', PostModel)When resolving foreign keys for a BaseModel instance, the manager determines the collection for a field using, in order:
- a provided
collectionsMapoverride (fieldName → collection) instance.getPropertyModel(field)- the field name itself
Register collection names that match your Firestore collections.
CRUD with models
- Create from an initialized model (writes generated id back to
key/idif empty):
const post = new PostModel('', /* ... */)
const id = await manager.create(post) // post.key is set- Update from an initialized model (merge true by default):
post.title = 'Updated'
await manager.update(post) // uses model.toFirestore()- Delete by instance or by collection/class+id:
await manager.deleteModel(post)
// or
await manager.deleteById({ cls: PostModel }, 'postId')
// or
await manager.deleteById('posts', 'postId')Recursive loading with depth control
Load a single doc by class and id. Control how deep to resolve foreign keys with depth:
// depth = 0: leaves foreign key fields as ids/strings
// depth = 1: resolves direct children (first level)
// depth = 2+: resolves deeper graphs up to the given depth (cycle-safe)
const post = await manager.loadById(PostModel, 'postId', { depth: 2 })Optionally override field→collection mapping when the field name/property model differs from the actual collection:
const post = await manager.loadById(PostModel, 'postId', {
depth: 1,
collectionsMap: { author: 'users', comments: 'comments' }
})Hydrate an existing, uninitialized instance (keeps reference identity):
const user = new UserModel('userId')
await manager.hydrateInstance(user, { depth: 1 })
// user instance is populated in-placeBackward-compatibility helpers:
getDocById(collection, id, cls?, resolveFKs?, collectionsMap?)behaves like before, but FK resolution now usesdepth = 1.resolveInstanceFKs(instance, collectionsMap?, eager?)defers to depth-based resolver.
Batched loading (parallel, per-depth aggregation)
Efficiently load multiple roots and resolve their foreign keys in batches. The manager aggregates ids per collection at each depth level, fetches them concurrently (respecting Firestore in query limit of 10 via chunking), assigns child models back, and proceeds to the next depth.
// Load multiple posts and resolve their first-level foreign keys
const posts = await manager.loadManyByIds(PostModel, ['a','b','c'], { depth: 1 })
// Hydrate multiple shell instances in place
const shells = [new PostModel('a'), new PostModel('b')]
await manager.hydrateManyInstances(shells, { depth: 2 })Notes:
- When the maximum depth is reached, foreign key fields remain as their original ids/strings (no further resolution).
- Cycles are handled via a visited set (
collection:id) to avoid infinite loops.
Using Firestore converters
You can use the built-in converter with Firestore SDK's withConverter:
const converter = manager.getConverter(PostModel)
const ref = db.collection('posts').withConverter(converter).doc('postId')
const snap = await ref.get()
const post = snap.data() // instance of PostModelIf you need recursive resolution after using a converter, call one of the resolver methods on the resulting instance(s), e.g., hydrateInstance(post, { depth: 1 }).
Collection mapping tips
- Prefer to set
propertyModelsin yourBaseModelsubclasses so the manager can infer the collection per field. - Use
collectionsMapwhen a field points to a collection with a different name than the field or its configured property model. - Always
registerModel(collectionName, Class)for any collection you want to resolve into model instances. Unregistered collections will be returned as plain objects (if loaded via batch APIs without a class) or remain as ids.
Packaging as npm module
To publish this as a package that other projects can install and use:
- Ensure
package.jsonfields are filled (author, repository, etc.) - Build the package:
npm run build(producesdist/) - Publish:
npm publish --access public(or use your CI to publish on tags)
Using in another project
Install the package and then in your project's build/CI script import the generator:
const { FirestoreRulesGenerator } = require('firestore-model-management')
const { metaToCASLRules } = require('firestore-model-management')
// build step that imports your compiled models and runs generation