Using EntityService
Type-safe entity operations for Firebase Realtime Database migrations.
Table of contents
- What is EntityService?
- Basic Setup
- CRUD Operations
- Complete Migration Examples
- Best Practices
- EntityService vs Raw Firebase API
- API Reference
- See Also
What is EntityService?
EntityService is a type-safe, CRUD-focused wrapper around Firebase Realtime Database operations designed specifically for use in migrations. It provides:
- π‘οΈ Type Safety - Generic type parameters for compile-time type checking
- π― Clean API - Simple CRUD methods instead of raw Firebase references
- π¦ Batch Operations - Built-in support for updating multiple entities
- π Query Support - Find entities by property values
- β¨ Smart Save - Automatic create or update based on entity key
When to use EntityService: Use EntityService when working with collections of similar objects (users, posts, products, etc.). For simple key-value operations or complex transactions, use the raw Firebase API directly.
Basic Setup
Define Your Entity Type
All entities must extend the IEntity interface which provides the key property:
import { IEntity } from '@migration-script-runner/firebase';
interface User extends IEntity {
key?: string; // From IEntity - Firebase auto-generated key
name: string;
email: string;
role: 'admin' | 'user';
createdAt: number;
}
Create EntityService Instance
In your migrationβs up() or down() method:
import { IRunnableScript, IMigrationInfo } from '@migration-script-runner/core';
import { IFirebaseDB, FirebaseHandler, EntityService } from '@migration-script-runner/firebase';
export default class YourMigration implements IRunnableScript<IFirebaseDB> {
async up(db: IFirebaseDB, info: IMigrationInfo, handler: FirebaseHandler): Promise<string> {
// Create EntityService for 'users' collection
const userService = new EntityService<User>(
db.database,
handler.cfg.buildPath('users')
);
// Now use userService methods...
}
}
Always use
handler.cfg.buildPath()to construct paths. This ensures path prefixing (shift) works correctly in multi-environment setups.
CRUD Operations
Create: Add New Entities
Create Single Entity
const userService = new EntityService<User>(db.database, handler.cfg.buildPath('users'));
// Create returns the auto-generated key
const newKey = await userService.create({
name: 'Alice',
email: 'alice@example.com',
role: 'admin',
createdAt: Date.now()
});
console.log(`Created user with key: ${newKey}`);
Create Multiple Entities
const newUsers = [
{ name: 'Bob', email: 'bob@example.com', role: 'user', createdAt: Date.now() },
{ name: 'Charlie', email: 'charlie@example.com', role: 'user', createdAt: Date.now() }
];
const keys = await Promise.all(
newUsers.map(user => userService.create(user))
);
return `Created ${keys.length} users`;
Read: Retrieve Entities
Get All Entities
// Returns array of entities with keys
const allUsers = await userService.getAll();
console.log(`Found ${allUsers.length} users`);
allUsers.forEach(user => {
console.log(`${user.key}: ${user.name}`);
});
Get All as Object
// Returns object with keys as properties
const usersObject = await userService.getAllAsObject();
// Access by key: usersObject[key]
Get Single Entity by Key
const user = await userService.get('user-key-123');
if (user) {
console.log(`Found user: ${user.name}`);
} else {
console.log('User not found');
}
Query by Property
// Find all admin users
const admins = await userService.findAllBy('role', 'admin');
console.log(`Found ${admins.length} admins`);
Update: Modify Existing Entities
Update Single Entity
// Update specific fields
await userService.update('user-key-123', {
email: 'newemail@example.com',
updatedAt: Date.now()
});
Smart Save (Create or Update)
const user: User = {
key: 'user-key-123', // If key exists, updates; if undefined, creates
name: 'Alice Updated',
email: 'alice@example.com',
role: 'admin',
createdAt: Date.now()
};
const key = await userService.save(user);
Batch Update All Entities
// Update function returns true if entity was modified
const results = await userService.updateAll((user) => {
// Skip users who already have the field
if (user.updatedAt) {
return false; // Not modified
}
// Add updatedAt field
user.updatedAt = Date.now();
return true; // Modified
});
console.log(`Updated: ${results.updated.length}, Skipped: ${results.skipped.length}`);
Delete: Remove Entities
Remove Single Entity
await userService.remove('user-key-123');
Remove Multiple Entities
const keysToRemove = ['key1', 'key2', 'key3'];
await userService.removeByIds(keysToRemove);
Remove All Entities
// β οΈ DANGER: Removes all entities in the collection
await userService.removeAll();
Complete Migration Examples
Example 1: Create Initial Data
import { IRunnableScript, IMigrationInfo } from '@migration-script-runner/core';
import { IFirebaseDB, FirebaseHandler, EntityService, IEntity } from '@migration-script-runner/firebase';
interface User extends IEntity {
name: string;
email: string;
role: 'admin' | 'user';
createdAt: number;
}
export default class CreateInitialUsers implements IRunnableScript<IFirebaseDB> {
async up(db: IFirebaseDB, info: IMigrationInfo, handler: FirebaseHandler): Promise<string> {
const userService = new EntityService<User>(db.database, handler.cfg.buildPath('users'));
const users: Omit<User, 'key'>[] = [
{ name: 'Admin', email: 'admin@example.com', role: 'admin', createdAt: Date.now() },
{ name: 'User1', email: 'user1@example.com', role: 'user', createdAt: Date.now() },
{ name: 'User2', email: 'user2@example.com', role: 'user', createdAt: Date.now() }
];
const keys = await Promise.all(users.map(user => userService.create(user)));
return `Created ${keys.length} users`;
}
async down(db: IFirebaseDB, info: IMigrationInfo, handler: FirebaseHandler): Promise<string> {
const userService = new EntityService<User>(db.database, handler.cfg.buildPath('users'));
await userService.removeAll();
return 'Removed all users';
}
}
Example 2: Add Field to All Entities
interface User extends IEntity {
name: string;
email: string;
role: 'admin' | 'user';
verified?: boolean; // New optional field
createdAt: number;
}
export default class AddVerifiedField implements IRunnableScript<IFirebaseDB> {
async up(db: IFirebaseDB, info: IMigrationInfo, handler: FirebaseHandler): Promise<string> {
const userService = new EntityService<User>(db.database, handler.cfg.buildPath('users'));
const results = await userService.updateAll((user) => {
if (user.verified !== undefined) {
return false; // Already has the field
}
user.verified = false;
return true;
});
return `Added verified field to ${results.updated.length} users (skipped ${results.skipped.length})`;
}
async down(db: IFirebaseDB, info: IMigrationInfo, handler: FirebaseHandler): Promise<string> {
const userService = new EntityService<User>(db.database, handler.cfg.buildPath('users'));
const results = await userService.updateAll((user) => {
if (user.verified === undefined) {
return false;
}
delete user.verified;
return true;
});
return `Removed verified field from ${results.updated.length} users`;
}
}
Example 3: Data Migration Between Collections
interface OldUser extends IEntity {
fullName: string;
emailAddress: string;
}
interface NewUser extends IEntity {
firstName: string;
lastName: string;
email: string;
migratedAt: number;
}
export default class MigrateUserStructure implements IRunnableScript<IFirebaseDB> {
async up(db: IFirebaseDB, info: IMigrationInfo, handler: FirebaseHandler): Promise<string> {
const oldUserService = new EntityService<OldUser>(db.database, handler.cfg.buildPath('old_users'));
const newUserService = new EntityService<NewUser>(db.database, handler.cfg.buildPath('users'));
const oldUsers = await oldUserService.getAll();
const migrationPromises = oldUsers.map(async (oldUser) => {
const [firstName, ...lastNameParts] = oldUser.fullName.split(' ');
await newUserService.create({
firstName,
lastName: lastNameParts.join(' '),
email: oldUser.emailAddress,
migratedAt: Date.now()
});
});
await Promise.all(migrationPromises);
return `Migrated ${oldUsers.length} users from old structure to new structure`;
}
async down(db: IFirebaseDB, info: IMigrationInfo, handler: FirebaseHandler): Promise<string> {
const newUserService = new EntityService<NewUser>(db.database, handler.cfg.buildPath('users'));
await newUserService.removeAll();
return 'Removed migrated users';
}
}
Example 4: Conditional Updates
interface User extends IEntity {
name: string;
email: string;
role: 'admin' | 'user';
lastLoginAt?: number;
status?: 'active' | 'inactive';
}
export default class MarkInactiveUsers implements IRunnableScript<IFirebaseDB> {
async up(db: IFirebaseDB, info: IMigrationInfo, handler: FirebaseHandler): Promise<string> {
const userService = new EntityService<User>(db.database, handler.cfg.buildPath('users'));
const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
const results = await userService.updateAll((user) => {
// Skip if already has status
if (user.status) {
return false;
}
// Mark as inactive if no login or old login
if (!user.lastLoginAt || user.lastLoginAt < thirtyDaysAgo) {
user.status = 'inactive';
} else {
user.status = 'active';
}
return true;
});
return `Updated ${results.updated.length} users with status`;
}
async down(db: IFirebaseDB, info: IMigrationInfo, handler: FirebaseHandler): Promise<string> {
const userService = new EntityService<User>(db.database, handler.cfg.buildPath('users'));
const results = await userService.updateAll((user) => {
if (!user.status) return false;
delete user.status;
return true;
});
return `Removed status from ${results.updated.length} users`;
}
}
Example 5: Query and Transform
interface User extends IEntity {
name: string;
email: string;
role: 'admin' | 'user' | 'moderator';
permissions?: string[];
}
export default class AddModeratorPermissions implements IRunnableScript<IFirebaseDB> {
async up(db: IFirebaseDB, info: IMigrationInfo, handler: FirebaseHandler): Promise<string> {
const userService = new EntityService<User>(db.database, handler.cfg.buildPath('users'));
// Find all moderators
const moderators = await userService.findAllBy('role', 'moderator');
// Add default permissions
const updatePromises = moderators.map(async (mod) => {
await userService.update(mod.key!, {
permissions: ['read', 'write', 'moderate']
});
});
await Promise.all(updatePromises);
return `Added permissions to ${moderators.length} moderators`;
}
async down(db: IFirebaseDB, info: IMigrationInfo, handler: FirebaseHandler): Promise<string> {
const userService = new EntityService<User>(db.database, handler.cfg.buildPath('users'));
const moderators = await userService.findAllBy('role', 'moderator');
const updatePromises = moderators.map(async (mod) => {
await userService.update(mod.key!, { permissions: [] });
});
await Promise.all(updatePromises);
return `Removed permissions from ${moderators.length} moderators`;
}
}
Best Practices
1. Always Use Type Parameters
// β
GOOD: Type-safe operations
const userService = new EntityService<User>(db.database, path);
const user = await userService.get(key); // user is typed as User
// β BAD: No type safety
const userService = new EntityService(db.database, path);
2. Handle Missing Entities
const user = await userService.get(key);
if (!user) {
throw new Error(`User ${key} not found`);
}
// Safe to use user here
console.log(user.name);
3. Use Batch Operations for Multiple Updates
// β
GOOD: Single updateAll call
await userService.updateAll((user) => {
user.verified = true;
return true;
});
// β BAD: Multiple individual updates (slower)
const users = await userService.getAll();
for (const user of users) {
await userService.update(user.key!, { verified: true });
}
4. Make Migrations Idempotent
// β
GOOD: Check before modifying
const results = await userService.updateAll((user) => {
if (user.verified !== undefined) {
return false; // Already has field
}
user.verified = false;
return true;
});
// β BAD: Overwrites every time
await userService.updateAll((user) => {
user.verified = false;
return true;
});
5. Use Path Prefixing
// β
GOOD: Uses buildPath for multi-environment support
const userService = new EntityService<User>(
db.database,
handler.cfg.buildPath('users')
);
// β BAD: Hardcoded path won't work with shift configuration
const userService = new EntityService<User>(db.database, 'users');
EntityService vs Raw Firebase API
When to Use EntityService
- β Working with collections of similar objects
- β Need type safety and IDE autocomplete
- β Performing CRUD operations on entities
- β Batch updating multiple entities
- β Querying by property values
When to Use Raw Firebase API
- β Complex queries with multiple conditions
- β
Single-node transactions with
ref.transaction() - β Real-time listeners and subscriptions
- β Multi-path atomic updates
- β Working with non-entity data (counters, flags, etc.)
Example Comparison
EntityService approach:
const userService = new EntityService<User>(db.database, handler.cfg.buildPath('users'));
const admins = await userService.findAllBy('role', 'admin');
Raw Firebase API approach:
const snapshot = await db.database
.ref(handler.cfg.buildPath('users'))
.orderByChild('role')
.equalTo('admin')
.once('value');
const admins = snapshot.val();
Both approaches work! EntityService provides type safety and cleaner code, while raw Firebase API offers more flexibility for complex operations.
API Reference
For complete API documentation, see:
- EntityService API - Full method reference
- IEntity Interface - Entity interface definition
- FirebaseDataService - Low-level operations
See Also
- Migration Scripts - General migration patterns
- Transactions - Understanding Firebase transaction limitations
- Best Practices - Firebase-specific patterns and tips
- Testing - Testing migrations with EntityService