npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@mzsn/firestore

v1.2.11

Published

TypeScript-first ORM library for Firestore with support for both client-side (web) and server-side (admin) environments

Readme

Firestore ORM

npm version npm downloads TypeScript Tree Shakeable

TypeScript-first ORM library for Firestore with support for both client-side (web) and server-side (admin) environments.

✨ Key Features:

  • ActiveRecord & Repository patterns - Intuitive document and collection management
  • Automatic change tracking - Only modified fields are saved via Proxy
  • Auto-generated IDs - Optional automatic ID generation with defaultKey
  • Lifecycle hooks - Built-in validation and timestamp management with beforeSave/afterSave
  • Works everywhere - Both firebase (web) and firebase-admin (server) SDKs
  • Multiple database support - Use multiple Firestore databases with databaseId
  • Path templates - Flexible document path configuration with placeholders

Installation

npm install @mzsn/firestore

For Web/Client Projects:

npm install @mzsn/firestore firebase

For Server/Functions Projects:

npm install @mzsn/firestore firebase-admin

Usage

Quick Start

// Auto-detects environment (browser → web, node → admin)
import { FirestoreDocument, FirestoreCollection, setupFirestore, newId } from '@mzsn/firestore';
import type { FirestoreKey, FirestoreData } from '@mzsn/firestore';

// Or explicitly specify (recommended for clarity)
// import { FirestoreDocument, FirestoreCollection, setupFirestore, newId } from '@mzsn/firestore/admin';
// import type { FirestoreKey, FirestoreData } from '@mzsn/firestore/admin';
import { initializeApp } from 'firebase-admin/app';
import { getFirestore } from 'firebase-admin/firestore';

// Initialize Firebase Admin
const app = initializeApp({ projectId: 'your-project-id' });
const db = getFirestore(app);
setupFirestore(db);

// Define your types
interface UserKey extends FirestoreKey {
  uid: string;
}

interface UserData extends FirestoreData {
  name: string;
  email: string;
  age: number;
  createdAt: Date;
  updatedAt: Date;
}

// Create a Document class with auto-generated IDs
class UserDocument extends FirestoreDocument<UserKey, UserData> {
  public static pathTemplate = 'users/{uid}';

  // Auto-generate ID for each new instance
  public static get defaultKey(): UserKey {
    return { uid: newId() };
  }

  public static get defaultData(): UserData {
    return {
      name: '',
      email: '',
      age: 0,
      createdAt: new Date(),
      updatedAt: new Date(),
    };
  }

  // Validation and auto-update timestamps
  protected override beforeSave(): void {
    // Update timestamp on every save
    this.data.updatedAt = new Date();

    // Validation
    if (!this.data.name || this.data.name.trim() === '') {
      throw new Error('Name is required');
    }
    if (!this.data.email || !this.data.email.includes('@')) {
      throw new Error('Valid email is required');
    }
    if (this.data.age < 0) {
      throw new Error('Age must be non-negative');
    }
  }
}

// Create a Collection class
class UserCollection extends FirestoreCollection<never, UserKey, UserData, UserDocument> {
  public static pathTemplate = 'users';
  public static documentClass = UserDocument;
}

// Example 1: Create with auto-generated ID
const newUser = new UserDocument();
newUser.data.name = 'John Doe';
newUser.data.email = '[email protected]';
newUser.data.age = 30;
await newUser.save();
console.log('Created:', newUser.id);  // Auto-generated ID

// Example 2: Create with manual ID
const specificUser = new UserDocument({ uid: 'user123' });
specificUser.data.name = 'Jane Doe';
specificUser.data.email = '[email protected]';
specificUser.data.age = 25;
await specificUser.save();

// Example 3: Load and update
const existingUser = new UserDocument({ uid: 'user123' });
await existingUser.get();
existingUser.data.age = 26;  // Automatically tracked
await existingUser.save();    // Only saves 'age' field

// Example 4: Query with Collection
const users = new UserCollection({
  where: [['age', '>=', 18]],
  orderBy: ['age', 'asc'],
  limit: 10
});
await users.get();

for (const [id, user] of users.documents) {
  console.log(`${id}: ${user.data.name} (${user.data.age})`);
}

// Example 5: Add via Collection
await users.add({
  name: 'Alice',
  email: '[email protected]',
  age: 28,
  createdAt: new Date(),
  updatedAt: new Date(),
});

// Example 6: Validation in action
try {
  const invalidUser = new UserDocument();
  invalidUser.data.name = '';  // Invalid - empty name
  invalidUser.data.email = 'invalid-email';  // Invalid - no @
  invalidUser.data.age = -5;  // Invalid - negative age
  await invalidUser.save();  // Throws error
} catch (error) {
  console.error('Validation failed:', error.message);
}

Web vs Admin SDK

The library provides separate implementations for client-side (web) and server-side (admin) with the same API.

Option 1: Auto-detect (recommended)

// Automatically uses the right version based on your environment:
// - Browser/Edge/Worker → web version
// - Node.js → admin version
import { FirestoreDocument, FirestoreCollection } from '@mzsn/firestore';

Option 2: Explicit import (for clarity)

// Admin SDK (Firebase Functions, Node.js, Cloud Functions)
import { FirestoreDocument, FirestoreCollection } from '@mzsn/firestore/admin';

// Web SDK (Browser, React, Vue, etc.)
import { FirestoreDocument, FirestoreCollection } from '@mzsn/firestore/web';

Web SDK Setup:

import { initializeApp } from 'firebase/app';
import { getFirestore, connectFirestoreEmulator } from 'firebase/firestore';
import { setupFirestore } from '@mzsn/firestore/web';

const app = initializeApp({
  apiKey: 'your-api-key',
  projectId: 'your-project-id',
  // ... other config
});

const db = getFirestore(app);

// For development with emulator
connectFirestoreEmulator(db, 'localhost', 8080);

// Initialize ORM
setupFirestore(db);

Admin SDK Setup:

import { initializeApp } from 'firebase-admin/app';
import { getFirestore } from 'firebase-admin/firestore';
import { setupFirestore } from '@mzsn/firestore/admin';

const app = initializeApp();
const db = getFirestore(app);

// For development with emulator
process.env.FIRESTORE_EMULATOR_HOST = 'localhost:8080';
db.settings({ host: 'localhost:8080', ssl: false });

// Initialize ORM
setupFirestore(db);

Multiple Database Support

You can use multiple Firestore databases by specifying a databaseId:

import { setupFirestore, FirestoreDocument } from '@mzsn/firestore/admin';
import { getFirestore } from 'firebase-admin/firestore';

// Register default database
const db = getFirestore();
setupFirestore(db); // or setupFirestore(db, '')

// Register additional databases
const dbSub = getFirestore('sub');
setupFirestore(dbSub, 'sub');

// Use specific database in Document class
class UserDocument extends FirestoreDocument<UserKey, UserData> {
  public static pathTemplate = 'users/{uid}';
  public static databaseId = ''; // Default database
}

class SubUserDocument extends FirestoreDocument<UserKey, UserData> {
  public static pathTemplate = 'users/{uid}';
  public static databaseId = 'sub'; // Secondary database
}

// Or create a subclass
class SubUserDocument extends UserDocument {
  public static databaseId = 'sub';
}

Auto-Generated Keys and Default Data

Use static getters for defaultKey and defaultData to generate dynamic values:

import { FirestoreDocument } from '@mzsn/firestore/admin';

// Auto-generate time-based IDs for each new instance
class Post extends FirestoreDocument<PostKey, PostData> {
  protected static pathTemplate = 'posts/{postId}';

  // Auto-generate unique, sortable, and verifiable ID for each new instance
  protected static get defaultKey(): PostKey {
    return { postId: timeId() };  // 26 chars: 13-digit timestamp + 13 random
  }

  // Fresh default data with current timestamp
  protected static get defaultData(): PostData {
    return {
      title: '',
      content: '',
      createdAt: new Date(),  // Fresh timestamp each time
    };
  }
}

// No need to specify key - auto-generated
const post = new Post();
post.data.title = 'My Post';
await post.save();  // Saved with auto-generated ID

// Singleton pattern - always same document
class AppConfig extends FirestoreDocument<ConfigKey, ConfigData> {
  protected static pathTemplate = 'config/{configId}';

  protected static get defaultKey(): ConfigKey {
    return { configId: 'app-settings' };  // Always references the same document
  }

  protected static get defaultData(): ConfigData {
    return {
      theme: 'light',
      language: 'en',
    };
  }
}

const config = new AppConfig();  // Always references 'app-settings'
await config.get();

Core Concepts

Path Templates

Define document paths using templates with placeholders:

import { FirestoreDocument } from '@mzsn/firestore/web';
import type { FirestoreKey, FirestoreData } from '@mzsn/firestore/web';

interface PostKey extends FirestoreKey {
  userId: string;
  postId: string;
}

interface PostData extends FirestoreData {
  title: string;
  content: string;
}

class Post extends FirestoreDocument<PostKey, PostData> {
  protected static pathTemplate = 'users/{userId}/posts/{postId}';
}

// Creates document at: users/user123/posts/post456
const post = new Post({ userId: 'user123', postId: 'post456' });

Change Tracking

All data access goes through a Proxy that automatically tracks changes:

const user = new User({ id: 'user123' });
await user.get();

console.log(user.isDirty);  // false

user.data.name = 'Jane';
user.data.age = 30;

console.log(user.isDirty);  // true

await user.save();  // Only sends {name: 'Jane', age: 30} to Firestore
console.log(user.isDirty);  // false

API Reference

FirestoreDocument

Constructor

constructor(key?, data?, exists?)
  • key: Document key object or path string
  • data: Initial data (optional)
  • exists: Whether document exists (default: false)

Properties

  • data: Data - Data proxy with change tracking
  • key: Key - Document key object
  • id: string - Document ID
  • exists: boolean - Whether document exists in Firestore
  • isNew: boolean - Whether document is new (not saved)
  • isDirty: boolean - Whether document has unsaved changes
  • isLoaded: boolean - Whether document has been loaded
  • reference: DocumentReference - Firestore reference

Methods

get(transaction?, cache?): Promise<this>

Load document from Firestore.

await user.get();  // Load from Firestore
await user.get(transaction);  // Load within transaction
await user.get(undefined, true);  // Use cache if already loaded
save(force?, transaction?): Promise<void>

Save changes to Firestore. Auto-detects whether to use set() or update().

await user.save();  // Update changed fields only
await user.save(true);  // Force full set operation
await user.save(false, transaction);  // Save within transaction
set(data?, transaction?): Promise<void>

Overwrite entire document.

await user.set({ name: 'John', age: 30, email: '[email protected]' });
await user.set(undefined, transaction);  // Set current data in transaction
update(transaction?): Promise<void>

Update only changed fields.

user.data.name = 'Jane';
await user.update();  // Only updates 'name' field
delete(transaction?): Promise<void>

Delete document from Firestore.

await user.delete();
await user.delete(transaction);
watch(callback?): () => void

Watch document for real-time updates. Returns unsubscribe function.

const unsubscribe = user.watch((data) => {
  console.log('Updated:', data);
});

// Stop watching
unsubscribe();
snapshot<T>(): AsyncGenerator<T>

Async generator for real-time updates.

for await (const user of new User({ id: 'user123' }).snapshot()) {
  console.log('Current:', user.data);
  if (someCondition) break;
}
toObject(): Data

Get a deep copy of document data.

const userData = user.toObject();

Lifecycle Hooks

Override these methods in subclasses:

import { FirestoreDocument } from '@mzsn/firestore/web';
import type { FirestoreKey, FirestoreData, FirestoreValue } from '@mzsn/firestore/web';

interface UserKey extends FirestoreKey {
  id: string;
}

interface UserData extends FirestoreData {
  name: string;
  email: string;
  age: number;
}

class User extends FirestoreDocument<UserKey, UserData> {
  protected static pathTemplate = 'users/{id}';

  // Validation before save
  protected beforeSave(): void {
    if (!this.data.email.includes('@')) {
      throw new Error('Invalid email');
    }
  }

  // Custom logic after save
  protected afterSave(): void {
    console.log('User saved:', this.id);
  }

  // Custom serialization
  protected serializeValue(value: FirestoreValue): FirestoreValue {
    // Date → Timestamp conversion is automatic
    return super.serializeValue(value);
  }

  // Custom deserialization
  protected unserializeValue(value: FirestoreValue): FirestoreValue {
    // Timestamp → Date conversion is automatic
    return super.unserializeValue(value);
  }
}

FirestoreCollection

Constructor

class UserCollection extends FirestoreCollection<never, UserKey, UserData, User> {
  protected static pathTemplate = 'users';  // Top-level collection
  protected static documentClass = User;
}

// For subcollections, define CollectionKey type
interface PostCollectionKey {
  userId: string;
}

class PostCollection extends FirestoreCollection<PostCollectionKey, PostKey, PostData, Post> {
  protected static pathTemplate = 'users/{userId}/posts';
  protected static documentClass = Post;
}

// Usage
const users = new UserCollection();  // All documents
const users = new UserCollection(condition);  // With query condition
const userPosts = new PostCollection({ userId: 'user123' });  // For subcollections

Parameters:

  • key?: CollectionKey - Collection key object for subcollections (optional)
  • condition?: Condition - Query conditions (optional)

Properties

  • documents: Map<string, Document> - Map of document ID to Document instance
  • reference: CollectionReference - Firestore collection reference
  • key: Key | string[] - Collection key
  • condition: Condition - Query condition
  • isLoaded: boolean - Whether documents have been loaded

Static Properties

  • pathTemplate: string - Path template for building collection paths (e.g., 'users' or 'users/{userId}/posts')
  • documentClass: DocumentConstructor - Document class constructor for creating document instances (required for inheritance pattern)

Methods

get(cache?): Promise<this>

Load documents matching query.

class UserCollection extends FirestoreCollection<never, UserKey, UserData, User> {
  protected static pathTemplate = 'users';
  protected static documentClass = User;
}

const users = new UserCollection({
  where: [['age', '>=', 18]],
  limit: 10
});
await users.get();

for (const [id, user] of users.documents) {
  console.log(id, user.data);
}

// Use cache to skip loading if already loaded
await users.get(true);
add(data?, transaction?): Promise<Document | undefined>

Create document with auto-generated ID.

const user = await users.add({
  name: 'John',
  age: 30,
  email: '[email protected]'
});
console.log('Created user:', user?.id);
set(id, data, transaction?): Promise<Document | undefined>

Create/update document with specific ID.

const user = await users.set('user123', {
  name: 'Jane',
  age: 25,
  email: '[email protected]'
});
delete(id, transaction?): Promise<void>

Delete document by ID.

await users.delete('user123');
save(transaction?): Promise<void>

Save all dirty documents in the collection.

// Make changes to multiple documents
users.documents.get('user1')!.data.age = 30;
users.documents.get('user2')!.data.age = 25;

// Save all changes
await users.save();
first(): Document | undefined

Get the first document in the collection.

const firstUser = users.first();
find(id): Document | undefined

Find a document by ID from loaded documents (cache only, doesn't query Firestore).

const user = users.find('user123');
toArray(): Document[]

Convert documents map to array.

const userArray = users.toArray();
docs(force?): Promise<Document[]>

Get all documents as an array, loading if necessary.

const userArray = await users.docs();
const freshUserArray = await users.docs(true); // Force reload
watch(callback): void

Watch collection for real-time updates.

users.watch((snapshot) => {
  console.log(`Collection has ${users.documents.size} documents`);
  console.log('Changes:', snapshot.docChanges());
});
unwatch(): void

Cancel all active snapshot listeners.

users.unwatch();
snapshot(): AsyncGenerator<Document[]>

Async generator for real-time collection updates.

for await (const documents of users.snapshot()) {
  console.log('Documents updated:', documents.length);
  for (const doc of documents) {
    console.log(doc.data);
  }
}

Batch Operations

Batch operations are provided as standalone functions that handle Firestore's 500-document batch limit automatically.

import { batchSave, batchDelete } from '@mzsn/firestore/admin';

batchSave(documents): Promise<void>

Save multiple documents in batches.

const documents = [user1, user2, user3, /* ... up to 1000s ... */];

// Automatically chunks into batches of 500
await batchSave(documents);

batchDelete(documents): Promise<void>

Delete multiple documents in batches.

const documentsToDelete = Array.from(users.documents.values());

// Automatically chunks into batches of 500
await batchDelete(documentsToDelete);

Query Conditions

// Where clause tuple: [fieldPath, operator, value]
type WhereClause = [fieldPath: string | FieldPath, opStr: WhereFilterOp, value: unknown];

interface Condition {
  where: WhereClause[];  // Required field
  orderBy?: [fieldPath: string | FieldPath, directionStr?: 'asc' | 'desc'];
  limit?: number;
  limitToLast?: number;
  startAfter?: DocumentSnapshot;
  startAt?: DocumentSnapshot;
  endBefore?: DocumentSnapshot;
  endAt?: DocumentSnapshot;
}

// Example
const condition: Condition = {
  where: [['age', '>=', 18]],
  orderBy: ['createdAt', 'desc'],
  limit: 10
};

// Just orderBy/limit - use empty where array
const condition2: Condition = {
  where: [],
  orderBy: ['name', 'asc'],
  limit: 5
};

Advanced Examples

Nested Collections

import { FirestoreDocument, FirestoreCollection } from '@mzsn/firestore/web';
import type { FirestoreKey, FirestoreData } from '@mzsn/firestore/web';

interface CommentKey extends FirestoreKey {
  userId: string;
  postId: string;
  commentId: string;
}

interface CommentData extends FirestoreData {
  text: string;
  author: string;
  createdAt: Date;
}

class Comment extends FirestoreDocument<CommentKey, CommentData> {
  protected static pathTemplate = 'users/{userId}/posts/{postId}/comments/{commentId}';
}

// For accessing a single comment
const comment = new Comment({
  userId: 'user123',
  postId: 'post456',
  commentId: 'comment789'
});

// For accessing a collection of comments
interface CommentCollectionKey {
  userId: string;
  postId: string;
}

class CommentCollection extends FirestoreCollection<CommentCollectionKey, CommentKey, CommentData, Comment> {
  protected static pathTemplate = 'users/{userId}/posts/{postId}/comments';
  protected static documentClass = Comment;
}

const comments = new CommentCollection({ userId: 'user123', postId: 'post456' });
await comments.get();

Transactions

import { getFirestore } from 'firebase/firestore';
import { runTransaction } from 'firebase/firestore';

await runTransaction(getFirestore(), async (transaction) => {
  const sender = new User({ id: 'user1' });
  const receiver = new User({ id: 'user2' });

  await sender.get(transaction);
  await receiver.get(transaction);

  sender.data.balance -= 100;
  receiver.data.balance += 100;

  await sender.save(false, transaction);
  await receiver.save(false, transaction);
});

Custom Validation

import { FirestoreDocument } from '@mzsn/firestore/web';
import type { FirestoreKey, FirestoreData } from '@mzsn/firestore/web';

interface UserKey extends FirestoreKey {
  id: string;
}

interface UserData extends FirestoreData {
  name: string;
  email: string;
  age: number;
}

class User extends FirestoreDocument<UserKey, UserData> {
  protected static pathTemplate = 'users/{id}';

  protected beforeSave(): void {
    if (this.data.age < 0) {
      throw new Error('Age must be positive');
    }
    if (!this.data.email.includes('@')) {
      throw new Error('Invalid email');
    }
  }
}

Real-time UI Updates

// React example
function UserProfile({ userId }: { userId: string }) {
  const [userData, setUserData] = useState<UserData | null>(null);

  useEffect(() => {
    const user = new User({ id: userId });
    user.get();

    const unsubscribe = user.watch((data) => {
      setUserData(data);
    });

    return unsubscribe;
  }, [userId]);

  return <div>{userData?.name}</div>;
}

Utility Functions

The library also exports utility functions:

import {
  deepEqual,
  parseKey,
  buildPath,
  newId,
  timeId,
  AsyncQueue
} from '@mzsn/firestore/web';

// Generate random ID
const id = newId(20);  // 20-character random ID

// Generate time-based ID (sortable and verifiable in Firestore Rules)
const timeBasedId = timeId();  // 26 chars: 13-digit timestamp + 13 random
const customLength = timeId(20);  // 20 chars: 13-digit timestamp + 7 random

// Deep equality check
const isEqual = deepEqual(obj1, obj2);

// Parse path to key
const key = parseKey('users/user123/posts/post456', 'users/{userId}/posts/{postId}');
// => { userId: 'user123', postId: 'post456' }

// Build path from key
const path = buildPath({ userId: 'user123', postId: 'post456' }, 'users/{userId}/posts/{postId}');
// => 'users/user123/posts/post456'

Firestore Rules Validation with timeId()

The timeId() format allows secure validation in Firestore Security Rules:

// firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Validate time-based IDs to prevent spoofing attacks
    match /posts/{postId} {
      allow create: if 
        // Check ID format (26 chars with 13-digit timestamp)
        postId.size() == 26 &&
        // Extract timestamp and verify it's not from the future
        int(postId.substring(0, 13)) <= request.time.toMillis() &&
        // Optional: reject IDs older than 1 minute (prevent replay attacks)
        int(postId.substring(0, 13)) >= request.time.toMillis() - 60000;
      
      allow read, update, delete: if true;
    }
  }
}

This prevents malicious users from:

  • Creating documents with future timestamps
  • Creating documents with very old timestamps (collision attacks)
  • Using arbitrary IDs that don't follow the time-based format

## Development

```bash
# Install dependencies
npm install

# Build
npm run build

# Clean
npm run clean

Running Tests

This project includes comprehensive test files for both web and admin implementations.

1. Start Firestore Emulator

In one terminal, start the Firestore emulator:

npm run emulator

This will start:

  • Firestore Emulator on localhost:8080
  • Emulator UI on http://localhost:4000

2. Run Tests

In another terminal, run the tests:

# Run admin (firebase-admin) tests
npm run test:admin

# Run web (firebase) tests
npm run test:web

# Run both
npm run test:admin && npm run test:web

Note: Make sure the emulator is running before executing tests, otherwise you'll get connection errors.

Test Coverage

Both test files include the following test cases:

  1. CRUD Operations

    • Creating documents with manual IDs
    • Creating documents with auto-generated IDs
    • Reading documents
    • Updating documents (dirty tracking)
    • Deleting documents
  2. Collection Operations

    • Adding multiple documents
    • Querying with conditions
    • Filtering and ordering
  3. Change Tracking

    • Verifying dirty state after load
    • Verifying dirty state after changes
    • Verifying dirty state after save
  4. Real-time Updates

    • Watching document changes
    • Receiving snapshot updates
    • Unsubscribing from listeners
  5. Collection Inheritance Pattern

    • Creating collections with keys
    • Querying with conditions
    • Direct usage without inheritance
    • Using string paths
    • Using string array keys

Test Files

Test files are located in the test/ directory:

  • test/admin/test.ts - Admin SDK integration tests
  • test/web/test.ts - Web SDK integration tests
  • test/firebase.json - Emulator configuration

Example test output:

✓ Connected to Firestore Emulator at localhost:8080

=== Testing CRUD Operations ===
Creating user with manual ID...
✓ User created: {
  name: 'John Doe',
  email: '[email protected]',
  age: 30,
  createdAt: 2025-10-20T15:00:54.953Z,
  updatedAt: 2025-10-20T15:00:54.953Z,
  _id: 'user123'
}

Creating user with auto-generated ID...
✓ Auto user created: {
  name: 'Auto User',
  email: '[email protected]',
  age: 25,
  createdAt: 2025-10-20T15:00:54.961Z,
  updatedAt: 2025-10-20T15:00:54.961Z,
  _id: 'gYgYmMi9TVTFOXxrEvmM'
}
✓ Auto-generated uid: gYgYmMi9TVTFOXxrEvmM

Updating user...
✓ Verified: {
  name: 'Jane Doe',
  age: 31,
  createdAt: 2025-10-20T15:00:54.953Z,
  updatedAt: 2025-10-20T15:00:54.973Z,  // Updated timestamp!
  _id: 'user123'
}

=== Testing Collection Operations ===
✓ Found 3 users:
  - Alice (25) [ZL6ql0Dn0td2suPDFeCl]
  - Charlie (28) [dblOR1bUfFTPpTQrP1i2]
  - Bob (35) [O1agEjATccrF072nYbfs]

✅ All tests passed!

Project Structure

firestore-orm/
├── shared/         # Common code (types, utils, async-queue)
├── web/            # Client-side implementation (firebase/firestore)
├── admin/          # Server-side implementation (firebase-admin/firestore)
├── test/           # Test files (not published to npm)
│   ├── admin/test.ts
│   ├── web/test.ts
│   └── firebase.json
└── dist/           # Compiled output
    ├── shared/
    ├── web/
    └── admin/

License

MIT