Documentation Index
Fetch the complete documentation index at: https://www.orionjs.com/llms.txt
Use this file to discover all available pages before exploring further.
Migrations in Orionjs are used to perform data transformations and schema updates on existing data. They run automatically via a background job and track completion state to ensure each migration runs only once.
Installation
pnpm add @orion-js/migrations
Structure and Naming
- Use
@MigrationService() decorator from @orion-js/migrations
- Each migration should be a separate class
- Follow naming convention:
Migrate{Entity}{Action}.v{Version} (e.g., MigrateUsersPlanType.v1)
- Implement a
runMigration() method that performs the migration logic
- Version suffix (
.v1, .v2) allows re-running modified migrations
- Put migrations in
app/{component}/migrations/Migrate{Entity}{Action}/index.ts
Best Practices
- Idempotency: Design migrations to be safely re-runnable when possible
- Progress logging: Log progress every N documents for long-running migrations
- Query optimization: Only fetch documents that need updating (use
$exists: false or similar filters)
- Batch processing: Process documents in a cursor loop, not loading all into memory
- Direct collection access: Migrations are the exception where direct collection access is allowed
- Use
useMongoTransactions: false unless you specifically need transaction support
- Use dependency injection to leverage existing services for business logic
- Define local interfaces for document shapes to avoid tight coupling with schema changes
Complete Example
import {logger} from '@orion-js/logger'
import {MigrationService} from '@orion-js/migrations'
import {createCollection} from '@orion-js/mongodb'
import {Inject} from '@orion-js/services'
import {CalculateCallerPlanTypeService} from 'app/ai/services/CalculateCallerPlanType'
/**
* Local interface for the document structure we're migrating.
* Keeps the migration decoupled from schema changes.
*/
interface GenerationLogCaller {
userId?: string
organizationId?: string
isAnonymous?: boolean
}
interface GenerationLogDoc {
_id: string
caller?: GenerationLogCaller
}
/**
* Migration to set planType and planId for all existing GenerationLogs.
* Uses current subscription status since all logs were generated recently.
*/
@MigrationService({
name: 'MigrateGenerationLogsPlanType.v1',
useMongoTransactions: false,
})
export class MigrateGenerationLogsPlanType {
// Direct collection access is allowed during migrations
private collection = createCollection({name: 'ai.generation_logs'})
@Inject(() => CalculateCallerPlanTypeService)
private calculateCallerPlanTypeService: CalculateCallerPlanTypeService
async runMigration() {
// Only fetch documents that need updating
const cursor = this.collection.find({
'caller.planType': {$exists: false},
})
let processed = 0
let updated = 0
// Process documents in a cursor loop to avoid memory issues
for await (const doc of cursor) {
const log = doc as GenerationLogDoc
const planInfo = await this.calculateCallerPlanTypeService.execute(log.caller)
await this.collection.updateOne(
{_id: log._id},
{
$set: {
'caller.planType': planInfo.planType,
...(planInfo.planId && {'caller.planId': planInfo.planId}),
},
},
)
updated++
processed++
// Log progress every 500 documents
if (processed % 500 === 0) {
logger.info('Migration progress', {processed, updated})
}
}
logger.info('MigrateGenerationLogsPlanType complete', {processed, updated})
}
}
Simple Migration Example
For simple field additions or transformations without external dependencies:
import {logger} from '@orion-js/logger'
import {MigrationService} from '@orion-js/migrations'
import {createCollection} from '@orion-js/mongodb'
/**
* Add default status field to all users without one.
*/
@MigrationService({
name: 'MigrateUsersAddDefaultStatus.v1',
useMongoTransactions: false,
})
export class MigrateUsersAddDefaultStatus {
private collection = createCollection({name: 'auth.users'})
async runMigration() {
const result = await this.collection.updateMany(
{status: {$exists: false}},
{$set: {status: 'active'}},
)
logger.info('MigrateUsersAddDefaultStatus complete', {
matchedCount: result.matchedCount,
modifiedCount: result.modifiedCount,
})
}
}
Loading Migrations
Migrations must be registered using loadMigrations() to enable automatic execution:
import {loadMigrations} from '@orion-js/migrations'
import {MigrateGenerationLogsPlanType} from './migrations/MigrateGenerationLogsPlanType'
import {MigrateUsersEmailFormat} from './migrations/MigrateUsersEmailFormat'
// Register all migrations - they will run automatically via background job
loadMigrations([
MigrateGenerationLogsPlanType,
MigrateUsersEmailFormat,
])
How Migrations Run
- Migrations are executed by a background job that polls every 30 seconds
- Only one migration runs at a time, in the order they are registered
- Completed migrations are tracked in the
orionjs.migrations collection
- The migration name serves as the unique identifier for tracking completion
- A lock prevents concurrent migration execution across multiple server instances
MongoDB Transactions
For operations that need to be atomic, you can use MongoDB transactions:
@MigrationService({
name: 'TransactionalMigration.v1',
useMongoTransactions: true
})
export class TransactionalMigration {
async runMigration() {
// All database operations will be in a transaction
// If any operation fails, all changes will be rolled back
}
}
Long-Running Migrations
For migrations that take a long time, extend the lock time:
async runMigration(context: ExecutionContext) {
// Extend lock time to two hours
context.extendLockTime(1000 * 60 * 60 * 2)
// Long-running operations...
}
Disabling Automatic Execution
If you want to manually control when migrations run:
loadMigrations([MigrationExample1, MigrationExample2], {
omitJob: true
})
Then manually trigger migrations:
import {getInstance} from '@orion-js/services'
import {MigrationsService} from '@orion-js/migrations'
import {createContext} from '@orion-js/dogs'
const migrationService = getInstance(MigrationsService)
const context = createContext()
await migrationService.runMigrations(migrations, context)
When to Create a New Migration Version
Create a new version (.v2, .v3, etc.) when:
- The original migration logic had a bug that needs fixing
- You need to re-run a migration with modified logic
- The migration name must change to run again (completed names are tracked)