Models & Decorators
FireODM uses TypeScript classes plus decorators to map your data types to Firestore documents. Every model:
- Extends BaseModel
- Uses
@Collection('name')
to define the Firestore collection - Annotates each field with a decorator for validation and conversion
1. Define the Collection
Use @Collection()
on your class:
import { BaseModel, Collection } from 'fireodm'
@Collection('users')
export class User extends BaseModel {
// fields...
}
This tells FireODM which Firestore collection to read from and write to.
2. Field Decorators
Each property in your model should have one decorator that enforces its type and validation:
- @StringField(opts?) – validates strings, supports
min
,max
,required
- @NumberField(opts?) – validates numbers, supports
min
,max
,required
- @EnumField(enumObj, opts?) – validates enums (predefined set of values), supports
required
,defaultValue
- @BooleanField(opts?) – validates booleans, supports
required
,defaultValue
- @TimestampField(opts?) – validates Firestore
Timestamp
, supportsdefaultNow
,required
- @GeoPointField(opts?) – validates Firestore
GeoPoint
, supportsrequired
- @ArrayField(schema, opts?) – validates arrays of a given Zod schema, supports
required
- @MapField(schema, opts?) – validates object maps, supports
required
- @DocumentReferenceField(opts?) – validates Firestore
DocumentReference
or populated model, supportsrequired
Example
import {
BaseModel,
Collection,
StringField,
NumberField,
BooleanField,
TimestampField,
ArrayField,
MapField
} from 'fireodm'
import { Timestamp } from 'firebase-admin/firestore'
@Collection('products')
export class Product extends BaseModel {
@StringField({ required: true, min: 3 })
name!: string;
@NumberField({ min: 0 })
price?: number;
@BooleanField({ defaultValue: true })
inStock!: boolean;
@TimestampField({ defaultNow: true })
addedAt?: Timestamp;
@ArrayField(z.string())
tags?: string[];
@MapField(z.number())
ratings?: Record<string, number>;
@EnumField(UserStatusEnum, { required: true, defaultValue: UserStatusEnum.ACTIVE })
status?: UserStatusEnum;
}
3. Relations
Link documents across collections with @Relation
and @DocumentReferenceField
:
import { Relation, DocumentReferenceField } from 'fireodm'
import { Department } from './department.model'
@Collection('employees')
export class Employee extends BaseModel {
@StringField()
name!: string
@DocumentReferenceField({ required: false })
@Relation(() => Department)
department?: DocumentReference | Department | null
}
@Relation(() => Department)
registers the related model forpopulate()
@DocumentReferenceField
ensures raw refs or instances pass validation
To fetch relations, use:
const emp = await Employee.findById(id, { populate: ['department'] })
or later:
await emp.populate(['department'])
4. Sub-Collections
FireODM also supports organizing related data in Firestore sub-collections using two decorators:
4.1. @SubcollectionModel(() => ParentModel, path)
Annotate a class to indicate it represents documents in a sub-collection under a parent document:
import { BaseModel, SubcollectionModel } from 'fireodm'
@SubcollectionModel(() => Parent, 'children')
export class ChildModel extends BaseModel {
@StringField()
name!: string
@NumberField()
age!: number
}
The path argument ('children'
) specifies the sub-collection name under its parent collection.
4.2 @Subcollection()
On your parent model, declare a property to hold sub-collection items and decorate it:
import { Subcollection } from 'fireodm'
export class ParentModel extends BaseModel {
@StringField()
title!: string
@Subcollection(() => ChildModel, 'children')
children?: ChildModel[]
}
@Subcollection()
registers metadata so FireODM knows to load children from /parents/{id}/children
.
4.3. Eager Loading with populateSub
Pass populateSub to findById or findAll to load sub-collections automatically:
// findById example
const parent = await ParentModel.findById('docId', {
populateSub: ['children'],
});
console.log(parent.children); // ChildModel[]
// findAll example
const { results } = await ParentModel.findAll({
limit: 10,
populateSub: ['children'],
});
results.forEach(p => console.log(p.children));
populateSub
takes an array of sub-collection property names to eagerly load.
4.4. Manual Loading with subcollection()
Use the instance method subcollection()
to fetch sub-collection data on demand:
const parent = await ParentModel.findById('docId')
// Load 'children' when needed:
const kids = await parent.subcollection('children')
console.log(kids); // ChildModel[]
5. Putting It All Together
Here is a complete User
example:
import { BaseModel, Collection, StringField, EmailField, Relation, DocumentReferenceField, TimestampField } from 'fireodm'
import { Timestamp, DocumentReference } from 'firebase-admin/firestore'
import { Department } from './department.model'
@Collection('users')
export class User extends BaseModel {
@StringField({ min: 2 })
name!: string
@EmailField()
email!: string
@TimestampField({ defaultNow: true })
createdAt?: Timestamp
@DocumentReferenceField({ required: false })
@Relation(() => Department)
department?: DocumentReference | Department | null
@SubCollection(() => Role, "roles")
roles?: Role[];
@EnumField(UserStatusEnum, { required: true, defaultValue: UserStatusEnum.ACTIVE })
status?: UserStatusEnum;
constructor(data: Partial<User>, id?: string) {
super(data, id)
}
}
@SubcollectionModel(() => User, 'roles')
export class Role extends BaseModel {
@StringField()
name!: string
@StringField()
description!: number
}
6. Next
Continue to Validation to learn more about Zod schemas and error handling in FireODM.