doba

With Valibot

Using doba with Valibot schemas and migration context.

Using Valibot for schema validation with legacy user migration and context tracking.

Full example

import * as v from 'valibot'
import { createRegistry } from 'dobajs'

const databaseUser = v.object({
  id: v.string(),
  email: v.pipe(v.string(), v.email()),
  passwordHash: v.string(),
  createdAt: v.pipe(v.string(), v.isoTimestamp()),
  settings: v.object({
    theme: v.picklist(['light', 'dark']),
    notifications: v.object({
      email: v.boolean(),
      push: v.boolean(),
    }),
    internal: v.object({
      lastLoginIp: v.string(),
      failedAttempts: v.number(),
    }),
  }),
})

const frontendUser = v.object({
  id: v.string(),
  email: v.pipe(v.string(), v.email()),
  createdAt: v.pipe(v.string(), v.isoTimestamp()),
  settings: v.object({
    theme: v.picklist(['light', 'dark']),
    notifications: v.object({
      email: v.boolean(),
      push: v.boolean(),
    }),
  }),
})

const aiUser = v.object({
  id: v.string(),
  email: v.string(),
  theme: v.string(),
  hasNotifications: v.boolean(),
})

const legacyUser = v.object({
  name: v.optional(v.string()),
  darkMode: v.optional(v.boolean()),
})

const registry = createRegistry({
  schemas: {
    database: databaseUser,
    frontend: frontendUser,
    ai: aiUser,
    legacy: legacyUser,
  },
  migrations: {
    'database->frontend': (user) => ({
      id: user.id,
      email: user.email,
      createdAt: user.createdAt,
      settings: {
        theme: user.settings.theme,
        notifications: user.settings.notifications,
      },
    }),
    'database->ai': (user) => ({
      id: user.id,
      email: user.email,
      theme: user.settings.theme,
      hasNotifications: user.settings.notifications.email || user.settings.notifications.push,
    }),
    'frontend->ai': (user) => ({
      id: user.id,
      email: user.email,
      theme: user.settings.theme,
      hasNotifications: user.settings.notifications.email || user.settings.notifications.push,
    }),
    'legacy->frontend': (user, ctx) => {
      ctx.defaulted(['id'], 'generated new id')
      ctx.defaulted(['createdAt'], 'set to current timestamp')
      ctx.defaulted(['settings', 'notifications'], 'defaulted to all false')

      let email = 'unknown@example.com'
      if (typeof user.name === 'string' && user.name.length > 0) {
        email = `${user.name.toLowerCase().replace(/\s+/g, '.')}@legacy.example.com`
        ctx.warn(`converted name "${user.name}" to email`)
      }

      return {
        id: `legacy-${Date.now()}`,
        email,
        createdAt: new Date().toISOString(),
        settings: {
          theme: user.darkMode === true ? 'dark' : 'light',
          notifications: { email: false, push: false },
        },
      }
    },
  },
})

Usage

// Direct transform
const dbUser = {
  /* ... */
}
const frontend = await registry.transform(dbUser, 'database', 'frontend')

// Legacy migration with context
const legacy = { name: 'Alice Johnson', darkMode: true }
const result = await registry.transform(legacy, 'legacy', 'frontend')

if (result.ok) {
  console.log(result.value)
  console.log(result.meta.defaults) // tracked defaulted fields
  console.log(result.meta.warnings) // warnings from ctx.warn()
}

// Multi-hop: legacy -> frontend -> ai
const ai = await registry.transform(legacy, 'legacy', 'ai')

The migration context (ctx) lets you track defaults and warnings without throwing. This metadata is available on result.meta after transform.

On this page