Skip to content

TypeScript Admin Guide

This guide provides a comprehensive overview of using FireSchema with the typescript-admin target, designed for Node.js backend environments using the Firebase Admin SDK.

Overview & Setup

Target Audience

Use this target if you are building:

  • Node.js backend services (e.g., Express, Cloud Functions, NestJS) that need privileged access to Firestore.
  • Admin dashboards or tools running in a trusted server environment.

Prerequisites

  • Completion of the Installation guide (CLI tool installed).
  • An existing Node.js TypeScript project.
  • The firebase-admin package installed: npm install firebase-admin
  • The @shtse8/fireschema-ts-admin-runtime package installed: npm install @shtse8/fireschema-ts-admin-runtime
  • Initialized Firebase Admin app and Firestore instance:
typescript
// Example _setup.ts (or similar init file)
import * as admin from 'firebase-admin';

let firestoreInstance: admin.firestore.Firestore;

export function initializeAdminEnvironment(): admin.firestore.Firestore {
    if (!firestoreInstance) {
        // Initialize Firebase Admin SDK (use appropriate credentials)
        // Option 1: Application Default Credentials (ADC)
        admin.initializeApp();
        // Option 2: Service Account Key File
        // import serviceAccount from './path/to/your/serviceAccountKey.json';
        // admin.initializeApp({ credential: admin.credential.cert(serviceAccount) });

        firestoreInstance = admin.firestore();

        // Optional: Connect to Firestore Emulator if using
        // firestoreInstance.settings({ host: "127.0.0.1:8080", ssl: false });
    }
    return firestoreInstance;
}

// Export the initialized instance
export const firestore = initializeAdminEnvironment();

SDK & Runtime

  • Supported SDK: firebase-admin (Node.js SDK)
  • Runtime Package: @shtse8/fireschema-ts-admin-runtime

Configuration (fireschema.config.json)

Ensure your configuration file specifies the typescript-admin target:

json
{
  "schema": "./firestore.schema.json",
  "outputs": [
    {
      "target": "typescript-admin",
      "outputDir": "./src/generated/firestore-admin", // Example
      "options": { "dateTimeType": "Timestamp" } // Or "Date"
    }
  ]
}

Generation

Run npx fireschema generate.


CRUD Operations

Basic Create, Read, Update, and Delete operations using the Admin SDK.

(Setup assumed from above)

typescript
import { UsersCollection } from '../generated/firestore-admin/users.collection'; // Adjust path
import { UserData, UserAddData } from '../generated/firestore-admin/users.types'; // Adjust path
import * as admin from 'firebase-admin';
import { DocumentReference, WriteResult } from 'firebase-admin/firestore';
const { FieldValue, Timestamp } = admin.firestore; // Destructure

const usersCollection = new UsersCollection(firestore);

Create (Add)

Use add() for new documents with auto-generated IDs. Requires UserAddData.

typescript
async function createUser(displayName: string, email: string): Promise<DocumentReference<UserData> | null> {
  const newUser: UserAddData = { displayName, email };
  try {
    const docRef = await usersCollection.add(newUser);
    console.log('Admin: User added:', docRef.id);
    return docRef;
  } catch (error) { console.error("Admin: Error adding user:", error); return null; }
}
// const newUserRef = await createUser('Admin Alice', 'admin.alice@example.com');

Create or Overwrite (Set)

Use set() for documents with specific IDs. Requires UserData unless merging. Returns Promise<WriteResult>.

typescript
async function createOrReplaceUser(userId: string, data: UserData): Promise<WriteResult | null> {
  try {
    const writeResult = await usersCollection.set(userId, data);
    console.log(`Admin: Doc ${userId} set at ${writeResult.writeTime.toDate()}.`);
    return writeResult;
  } catch (error) { console.error(`Admin: Error setting doc ${userId}:`, error); return null; }
}
const bobData: UserData = {
  id: 'admin-bob-123',
  displayName: 'Admin Bob',
  email: 'admin.bob@example.com',
  isActive: true,
  status: 'active', // Assuming type exists
  createdAt: Timestamp.now(), // Use admin Timestamp
  updatedAt: Timestamp.now(),
};
// await createOrReplaceUser('admin-bob-123', bobData);

// Set with Merge (Upsert)
async function upsertUserPartial(userId: string, partialData: Partial<UserData>): Promise<WriteResult | null> {
   try {
    const writeResult = await usersCollection.set(userId, partialData, { merge: true });
    console.log(`Admin: Doc ${userId} upserted at ${writeResult.writeTime.toDate()}.`);
    return writeResult;
  } catch (error) { console.error(`Admin: Error upserting doc ${userId}:`, error); return null; }
}
// await upsertUserPartial('admin-charlie-456', { displayName: 'Admin Charlie', age: 30 });

Read (Get)

Use get() to retrieve a single document by ID. Returns Promise<UserData | null>.

typescript
async function getUser(userId: string): Promise<UserData | null> {
  try {
    const userData = await usersCollection.get(userId);
    if (userData) { console.log('Admin: User found:', userData.displayName); return userData; }
    else { console.log(`Admin: User ${userId} not found.`); return null; }
  } catch (error) { console.error(`Admin: Error getting user ${userId}:`, error); return null; }
}
// const user = await getUser('admin-alice-abc');

Update

Use update() to get an AdminUpdateBuilder. Call .commit() to execute (Promise<WriteResult>).

typescript
async function updateUserLogin(userId: string): Promise<WriteResult | null> {
  try {
    const writeResult = await usersCollection.update(userId)
      .incrementLoginCount(1) // Assumes field exists
      .setLastLogin(FieldValue.serverTimestamp()) // Use admin FieldValue
      .commit();
    console.log(`Admin: Updated login for ${userId} at ${writeResult.writeTime.toDate()}.`);
    return writeResult;
  } catch (error) { console.error(`Admin: Error updating user ${userId}:`, error); return null; }
}
// await updateUserLogin('admin-bob-123');

(See Advanced Updates below)

Delete

Use delete() to remove a document by ID. Returns Promise<WriteResult>.

typescript
async function deleteUser(userId: string): Promise<WriteResult | null> {
  try {
    const writeResult = await usersCollection.delete(userId);
    console.log(`Admin: User ${userId} deleted at ${writeResult.writeTime.toDate()}.`);
    return writeResult;
  } catch (error) { console.error(`Admin: Error deleting user ${userId}:`, error); return null; }
}
// await deleteUser('admin-charlie-456');

Querying Data

Build and execute queries using the AdminQueryBuilder.

(Setup assumed from above)

typescript
import { QuerySnapshot, DocumentSnapshot } from 'firebase-admin/firestore';

Filtering (where[FieldName])

Use generated methods for type-safe filtering.

typescript
const adminQuery = usersCollection.query()
  .whereIsActive("==", true) // Assumes 'isActive' field
  .whereRole("==", "admin");   // Assumes 'role' field

Ordering (orderBy)

Sort results. Requires indexes for complex sorts.

typescript
const orderedQuery = usersCollection.query().orderBy("email", "desc");

Limiting (limit, limitToLast)

Restrict the number of results.

typescript
const limitQuery = usersCollection.query().orderBy("email").limit(100);

Pagination (startAfter, etc.)

Use admin.firestore.DocumentSnapshot from query.get().

typescript
let lastAdminDoc: DocumentSnapshot<UserData> | null = null;
async function loadMoreAdminUsers() {
  let queryBuilder = usersCollection.query().orderBy("email").limit(50);
  if (lastAdminDoc) { queryBuilder = queryBuilder.startAfter(lastAdminDoc); }
  const snapshot = await queryBuilder.get(); // Use get() for snapshot
  if (!snapshot.empty) { lastAdminDoc = snapshot.docs[snapshot.docs.length - 1]; }
  else { lastAdminDoc = null; }
  const users = usersCollection.converter.fromFirestoreQuerySnapshot(snapshot);
  // ... process users ...
}

Executing (getData, get)

  • getData(): Returns Promise<UserData[]>.
  • get(): Returns Promise<QuerySnapshot<UserData>>.
typescript
const admins = await adminQuery.getData();
const snapshot = await orderedQuery.limit(10).get();

Streaming / Realtime Updates

The Firebase Admin SDK does not support real-time listeners (onSnapshot).


Working with Subcollections

Access subcollections via generated methods on the parent Collection instance.

(Setup assumed from above)

typescript
import { PostsCollection } from '../generated/firestore-admin/users/posts.collection'; // Adjust path
import { PostAddData } from '../generated/firestore-admin/users/posts.types';
typescript
const userId = 'admin-user-123';
const userPostsCollection: PostsCollection = usersCollection.posts(userId); // Get subcollection ref

const newPostData: PostAddData = { title: 'Admin Post', content: '...' };
const postRef = await userPostsCollection.add(newPostData);
const posts = await userPostsCollection.query().limit(5).getData();

Advanced Updates

Use the AdminUpdateBuilder (from collection.update(id)) for atomic operations.

(Setup assumed from above)

Generated Helpers

typescript
await usersCollection.update(userId)
  .incrementLoginCount(1) // Assumes field exists
  .setUpdatedAtToServerTimestamp() // Assumes field exists
  .commit();

Using Raw FieldValue Operations

For nested fields or operations not covered by generated helpers, you can use the standard Admin SDK update method on the raw DocumentReference with FieldValue operations directly.

typescript
// Get the raw DocumentReference
const userDocRef = usersCollection.docRef(userId);

await userDocRef.update({
  'settings.theme': 'monokai',
  'stats.adminLogins': FieldValue.increment(1), // Use imported FieldValue
  roles: FieldValue.arrayUnion('moderator'),   // Use imported FieldValue
  legacyId: FieldValue.delete()                // Use imported FieldValue
});

Combining Updates

You can combine generated helpers with raw updates by committing the builder first, then performing the raw update, or vice-versa if appropriate for your logic. However, they cannot be chained within the same UpdateBuilder instance.

typescript
// Example: Use builder for some fields, then raw update for others
await usersCollection.update(userId)
  .setIsActive(true)
  .commit(); // Commit builder changes first

await usersCollection.docRef(userId).update({ // Perform raw update separately
  roles: FieldValue.arrayUnion('verified-admin')
});

Transactions & Batched Writes

Use standard Admin SDK functions (firestore.runTransaction, firestore.batch) with raw references and converters.

(Setup assumed from above)

typescript
import { Transaction, WriteBatch } from 'firebase-admin/firestore';
import { ProductsCollection } from '../generated/firestore-admin/products.collection'; // Example

Transactions

typescript
await firestore.runTransaction(async (transaction: Transaction) => {
  const userRef = usersCollection.docRef(userId); // Raw ref
  const productRef = productsCollection.docRef(productId); // Raw ref
  const userSnap = await transaction.get(userRef); // Read
  const productSnap = await transaction.get(productRef); // Read
  if (!userSnap.exists || !productSnap.exists) throw new Error("Not found");
  const userData = userSnap.data(); // Typed data
  const productData = productSnap.data(); // Typed data
  // ... logic ...
  transaction.update(userRef, { balance: FieldValue.increment(-cost) }); // Write
  transaction.update(productRef, { stock: FieldValue.increment(-qty) }); // Write
});

Batched Writes

typescript
const batch: WriteBatch = firestore.batch();
const userRef = usersCollection.docRef(userId);
const newUserRef = usersCollection.docRef(); // New doc ref
const newUser: UserAddData = { /* ... */ };

// Use converter for set with typed data
batch.set(newUserRef, usersCollection.converter.toFirestore(newUser));
batch.update(userRef, { 'profile.lastAction': FieldValue.serverTimestamp() });
batch.delete(usersCollection.docRef('old-user-id'));

await batch.commit();

(Refer to the official Firebase Admin SDK documentation for more complex transaction/batch patterns)


Testing Strategy

  • Unit Tests: Use Jest mocks to test application logic without hitting Firestore.
  • Integration Tests: Use the Firestore Emulator and Jest. Instantiate generated Collection classes with an emulator-connected Firestore instance (from firebase-admin) to test real interactions. This is the primary method used for testing the @shtse8/fireschema-ts-admin-runtime package.

Released under the ISC License.