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:
// 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:
{
"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)
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
.
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>
.
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>
.
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>
).
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>
.
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)
import { QuerySnapshot, DocumentSnapshot } from 'firebase-admin/firestore';
Filtering (where[FieldName]
)
Use generated methods for type-safe filtering.
const adminQuery = usersCollection.query()
.whereIsActive("==", true) // Assumes 'isActive' field
.whereRole("==", "admin"); // Assumes 'role' field
Ordering (orderBy
)
Sort results. Requires indexes for complex sorts.
const orderedQuery = usersCollection.query().orderBy("email", "desc");
Limiting (limit
, limitToLast
)
Restrict the number of results.
const limitQuery = usersCollection.query().orderBy("email").limit(100);
Pagination (startAfter
, etc.)
Use admin.firestore.DocumentSnapshot
from query.get()
.
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()
: ReturnsPromise<UserData[]>
.get()
: ReturnsPromise<QuerySnapshot<UserData>>
.
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)
import { PostsCollection } from '../generated/firestore-admin/users/posts.collection'; // Adjust path
import { PostAddData } from '../generated/firestore-admin/users/posts.types';
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
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.
// 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.
// 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)
import { Transaction, WriteBatch } from 'firebase-admin/firestore';
import { ProductsCollection } from '../generated/firestore-admin/products.collection'; // Example
Transactions
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
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-connectedFirestore
instance (fromfirebase-admin
) to test real interactions. This is the primary method used for testing the@shtse8/fireschema-ts-admin-runtime
package.