With Zod
Using doba with Zod schemas, versioned migrations, and path finding.
A full example using Zod for schema validation, including nested objects, schema versioning, and multi-hop path finding.
Setup
Define your schemas
Each schema represents a shape your data can take. Here we have a database layer with internal fields, a clean frontend shape, an AI-friendly projection, and two frontend versions.
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(),
settings: z.object({
theme: z.enum(['light', 'dark']),
notifications: z.object({
email: z.boolean(),
push: z.boolean(),
sms: z.boolean(),
}),
internal: z.object({
lastLoginIp: z.string(),
failedAttempts: z.number(),
}),
}),
})
const frontendUser = z.object({
id: z.string(),
email: z.string().email(),
createdAt: z.string().datetime(),
settings: z.object({
theme: z.enum(['light', 'dark']),
notifications: z.object({
email: z.boolean(),
push: z.boolean(),
sms: z.boolean(),
}),
}),
})
const aiUser = z.object({
id: z.string(),
email: z.string(),
theme: z.string(),
notificationsEnabled: z.boolean(),
})
const frontendV1 = z.object({
id: z.string(),
email: z.string(),
theme: z.string(),
})
const frontendV2 = z.object({
id: z.string(),
email: z.string(),
createdAt: z.string().datetime(),
settings: z.object({
theme: z.enum(['light', 'dark']),
notifications: z.object({
email: z.boolean(),
push: z.boolean(),
sms: z.boolean(),
}),
}),
})Write migrations
Migrations connect schemas. doba builds a graph and finds the shortest path automatically.
const registry = createRegistry({
schemas: {
database: databaseUser,
frontend: frontendUser,
ai: aiUser,
'frontend:v1': frontendV1,
'frontend:v2': frontendV2,
},
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,
notificationsEnabled:
user.settings.notifications.email ||
user.settings.notifications.push ||
user.settings.notifications.sms,
}),
'frontend->ai': (user) => ({
id: user.id,
email: user.email,
theme: user.settings.theme,
notificationsEnabled:
user.settings.notifications.email ||
user.settings.notifications.push ||
user.settings.notifications.sms,
}),
'frontend:v1->frontend:v2': (user, ctx) => {
ctx.defaulted(['settings', 'notifications'], 'defaulting all to true')
ctx.warn('createdAt set to current timestamp')
return {
id: user.id,
email: user.email,
createdAt: new Date().toISOString(),
settings: {
theme: user.theme === 'light' || user.theme === 'dark' ? user.theme : 'light',
notifications: { email: true, push: true, sms: false },
},
}
},
'frontend:v2->frontend:v1': (user) => ({
id: user.id,
email: user.email,
theme: user.settings.theme,
}),
'frontend->frontend:v2': (user) => user,
'frontend:v2->frontend': (user) => user,
},
})Transform data
const dbUser = {
id: 'user-123',
email: 'alice@example.com',
passwordHash: 'hashed_password_xyz',
createdAt: '2024-01-15T10:30:00Z',
settings: {
theme: 'dark' as const,
notifications: { email: true, push: false, sms: false },
internal: { lastLoginIp: '192.168.1.1', failedAttempts: 0 },
},
}
// Direct migration
const frontend = await registry.transform(dbUser, 'database', 'frontend')
// Multi-hop: frontend:v1 -> frontend:v2 -> frontend -> ai
const v1User = { id: 'u-1', email: 'bob@example.com', theme: 'dark' }
const ai = await registry.transform(v1User, 'frontend:v1', 'ai')
if (ai.ok) {
console.log(ai.value) // { id, email, theme, notificationsEnabled }
console.log(ai.meta.path) // ["frontend:v1", "frontend:v2", "frontend", "ai"]
}Path finding is automatic. You don't need to specify intermediate schemas. doba finds the shortest route through the graph using BFS.