Migrations
Migrations let you evolve your database schema and seed data over time. They're named, reversible, and automatically tracked.
Creating a Migration​
import { createMigration } from '@sajjadbzn/jcell'
const m001 = createMigration('001_create_users', {
async up(db) {
// Define the schema
const userSchema = schema({
id: t.id(),
name: t.string(),
email: t.string().index({ unique: true }),
role: t.enum(['admin', 'user'] as const),
createdAt: t.date().default(() => new Date()),
})
// Register the collection (this creates the table/indexes)
db.collection('users', userSchema)
// Seed initial data
const users = db.collection('users', userSchema)
await users.insert({ name: 'Admin', email: 'admin@example.com', role: 'admin' })
},
async down(db) {
// Rollback (e.g., drop collection data)
// Note: this is optional — you can omit it if rollback isn't needed
},
})
Running Migrations​
const db = createDB({ adapter: sqliteAdapter({ path: './app.db' }) })
await db.migrate([m001, m002, m003])
- Only unapplied migrations are executed
- Applied migrations are tracked in the
_migrationscollection - Order is determined by the array — run earlier migrations first
How It Works​
- jcell creates a
_migrationscollection with schema:{ id: string, name: string, appliedAt: Date } - On
db.migrate([...]), jcell queries_migrationsto find already-applied migrations - For each unapplied migration, it calls
migration.up(db)and records the name - If
migration.down()is defined, it can be used to roll back
Complete Example​
import { createDB, createMigration, schema, t, sqliteAdapter } from '@sajjadbzn/jcell'
// Migration 001: Create users table
const m001 = createMigration('001_create_users', {
async up(db) {
const userSchema = schema({
id: t.id(),
name: t.string(),
email: t.string().index({ unique: true }),
role: t.enum(['admin', 'user'] as const),
})
db.collection('users', userSchema)
const users = db.collection('users', userSchema)
await users.insert({ name: 'Admin', email: 'admin@test.com', role: 'admin' })
},
})
// Migration 002: Add profile data
const m002 = createMigration('002_add_profiles', {
async up(db) {
const profileSchema = schema({
id: t.id(),
userId: t.ref('users'),
bio: t.string().optional(),
avatar: t.string().optional(),
})
db.collection('profiles', profileSchema)
},
})
// Run migrations
const db = createDB({ adapter: sqliteAdapter({ path: './app.db' }) })
await db.migrate([m001, m002])
console.log('Migrations complete! ✅')
Best Practices​
- Name migrations meaningfully — use descriptive names like
001_initial_schemaor002_add_email_index - Make migrations idempotent — use
db.collection()which returns existing collections if already registered - Always define
up— the migration must apply forward - Define
downfor rollbacks — helpful during development - Run migrations in order — the array order determines execution order
- Use adapters that persist — the
_migrationscollection needs persistence (SQLite, file)
note
Migrations work with any adapter that persists data (file, SQLite, D1). For the memory adapter, migrations are re-run on every restart since the _migrations collection is ephemeral.