doba

Reversible Migrations

Bidirectional migrations with the database<->frontend syntax.

doba supports reversible (bidirectional) migrations using the <-> arrow syntax. Define both forward and backward transforms in one declaration.

Full example

import { z } from 'zod'
import { createRegistry } from 'dobajs'

const databaseUser = z.object({
  id: z.string(),
  email: z.string().email(),
  passwordHash: z.string(),
  createdAt: z.string().datetime(),
  role: z.enum(['admin', 'user']),
})

const frontendUser = z.object({
  id: z.string(),
  email: z.string().email(),
  createdAt: z.string().datetime(),
  role: z.enum(['admin', 'user']),
})

const aiUser = z.object({
  id: z.string(),
  email: z.string(),
  isAdmin: z.boolean(),
})

const registry = createRegistry({
  schemas: {
    database: databaseUser,
    frontend: frontendUser,
    ai: aiUser,
  },
  migrations: {
    // Reversible migration: both directions in one declaration
    'database<->frontend': {
      forward: (user) => ({
        id: user.id,
        email: user.email,
        createdAt: user.createdAt,
        role: user.role,
      }),
      backward: (user, ctx) => {
        ctx.defaulted(['passwordHash'], 'set to empty string')
        return {
          id: user.id,
          email: user.email,
          passwordHash: '',
          createdAt: user.createdAt,
          role: user.role,
        }
      },
      label: 'db-frontend-sync',
    },

    // One-way migration
    'frontend->ai': (user) => ({
      id: user.id,
      email: user.email,
      isAdmin: user.role === 'admin',
    }),
  },
})

Usage

const dbUser = {
  id: 'user-123',
  email: 'alice@example.com',
  passwordHash: 'hashed_abc',
  createdAt: '2024-01-15T10:30:00Z',
  role: 'admin' as const,
}

// Forward: database -> frontend
const frontend = await registry.transform(dbUser, 'database', 'frontend')

// Backward: frontend -> database
const feUser = {
  id: 'user-123',
  email: 'alice@example.com',
  createdAt: '2024-01-15T10:30:00Z',
  role: 'admin' as const,
}
const db = await registry.transform(feUser, 'frontend', 'database')

if (db.ok) {
  console.log(db.value) // includes passwordHash: ""
  console.log(db.meta.defaults) // [{ path: ["passwordHash"], message: "..." }]
}

// Multi-hop: database -> frontend -> ai
const ai = await registry.transform(dbUser, 'database', 'ai')
if (ai.ok) {
  console.log(ai.meta.path) // ["database", "frontend", "ai"]
}

When both a one-way (->) and reversible (<->) migration exist for the same direction, the one-way migration takes priority.

On this page