Create a new database migration file. Arguments: $ARGUMENTS
Migration File Location
Create migration file under apps/core/src/migration/version/.
Naming format: v<major>.<minor>.<patch>.ts (e.g., v9.1.0.ts)
Migration File Template
import type { Db } from 'mongodb'
/**
- Migration description:
- Changes:
-
*/ export default async function v(db: Db) { // 1. Check if migration is needed const existingData = await db.collection('').findOne({ // query conditions })
if (!existingData) { return // No migration needed }
// 2. Check if already migrated if (existingData.newField !== undefined) { return // Already migrated }
// 3. Execute migration await db.collection('').updateMany( { /* query conditions / }, { $set: { / update content */ } }, )
// Or batch update const cursor = db.collection('').find({}) for await (const doc of cursor) { await db.collection('').updateOne( { _id: doc._id }, { $set: { /* update content */ } }, ) } }
## Common Migration Scenarios
### 1. Rename Field
```typescript
export default async function v910(db: Db) {
await db.collection('posts').updateMany(
{ oldFieldName: { $exists: true } },
{ $rename: { oldFieldName: 'newFieldName' } },
)
}
2. Add Default Value
export default async function v910(db: Db) {
await db.collection('posts').updateMany(
{ newField: { $exists: false } },
{ $set: { newField: 'defaultValue' } },
)
}
3. Data Format Transformation
export default async function v910(db: Db) {
const cursor = db.collection('options').find({ name: 'config' })
for await (const doc of cursor) {
const { oldFormat, ...rest } = doc.value
const newValue = {
...rest,
newFormat: transformData(oldFormat),
}
await db.collection('options').updateOne(
{ _id: doc._id },
{ $set: { value: newValue } },
)
}
}
4. Remove Field
export default async function v910(db: Db) {
await db.collection('users').updateMany(
{},
{ $unset: { deprecatedField: '' } },
)
}
5. Create Index
export default async function v910(db: Db) {
await db.collection('posts').createIndex(
{ slug: 1 },
{ unique: true },
)
}
Register Migration
Add migration in apps/core/src/migration/history.ts:
import v910 from './version/v9.1.0'
export const VersionList = [
// ... other migrations
{ name: 'v9.1.0', run: v910 },
]
Migration Execution
Migrations run automatically on application startup. Executed migrations are recorded in mx_migration collection and won't run again.
Testing Migration
Create test file: apps/core/test/src/migration/v<version>.spec.ts
import { describe, expect, it, beforeAll, afterAll } from 'vitest'
import { Db } from 'mongodb'
import v910 from '~/migration/version/v9.1.0'
import { dbHelper } from 'test/helper/db-mock.helper'
describe('Migration v9.1.0', () => {
let db: Db
beforeAll(async () => {
const connection = await dbHelper.connect()
db = connection.connection.db!
})
afterAll(async () => {
await dbHelper.close()
})
it('should migrate old format to new format', async () => {
// Insert old format data
await db.collection('test').insertOne({
oldField: 'value',
})
// Execute migration
await v910(db)
// Verify result
const result = await db.collection('test').findOne({})
expect(result).toMatchObject({
newField: 'transformedValue',
})
expect(result!.oldField).toBeUndefined()
})
it('should skip if already migrated', async () => {
// Insert new format data
await db.collection('test').insertOne({
newField: 'value',
})
// Migration should not error
await v910(db)
// Data should remain unchanged
const result = await db.collection('test').findOne({})
expect(result!.newField).toBe('value')
})
})
Notes
- Migrations should be idempotent (can be run repeatedly without side effects)
- Check if migration is needed first to avoid unnecessary operations
- Use cursor for large datasets to avoid memory overflow
- Failed migrations throw exceptions and prevent app startup
- Backup data before running migrations in production