Debugging
Tools for understanding what your registry is doing and why.
Debugging
When something goes wrong, you want the tools to tell you what happened, not just "no". doba gives you several ways to see what's going on.
debug mode
The fastest way to see what's happening. Pass debug: true to createRegistry and it logs everything to the console with a [doba] prefix.
const registry = createRegistry({
schemas: {
/* ... */
},
migrations: {
/* ... */
},
debug: true,
})Then just run your code:
[doba] warn v1->v2: defaulted email: generated from name
[doba] step 1/2 v1->v2 "add-email" [ok] 0.5ms
[doba] step 2/2 v2->v3 "rename-fields" [ok] 0.0ms
[doba] transform v1->v3 [ok] 4.6ms (v1 -> v2 -> v3)That's it. Warnings, per-step timing, the full path it took, and whether it worked. If you have your own hooks defined, they still fire too. Debug mode just adds the console output on top.
Turn it off when you're done. Or leave it on during development, whatever works for you.
explain()
The explain method gives you a full breakdown of what path the registry would take between two schemas, without actually running any migrations.
const info = registry.explain('v1', 'v3')
console.log(info.summary)
// Path: v1 -> v2 -> v3 (2 steps, total cost: 0)
// 1. v1 -> v2 (cost: 0) [v1-to-v2-upgrade]
// 2. v2 -> v3 (cost: 0) [v2-to-v3-upgrade]What you get back
type ExplainResult = {
from: string
to: string
path: string[] | null // the resolved path, or null
totalCost: number | null // sum of edge costs
steps: ExplainStep[] // per-step breakdown
summary: string // human-readable summary
}
type ExplainStep = {
from: string
to: string
cost: number
label?: string // from migration metadata
deprecated?: string | boolean
}When there's no path
Instead of just "no path found", it tells you what IS reachable and what's missing:
const info = registry.explain('v3', 'v1')
console.log(info.summary)
// No migration path from "v3" to "v1".
// Reachable from "v3": flat.
// No schema has a path to "v1".That immediately tells you: v3 can only go to flat, and nothing points at v1. You know exactly which migration to add.
Same schema
registry.explain('v2', 'v2')
// { summary: '"v2" is already the target schema.', totalCost: 0, steps: [] }Better error messages
When a transform fails because no path exists, the error now includes the same reachability info that explain provides.
const result = await registry.transform(data, 'v3', 'v1')
if (!result.ok) {
console.log(result.issues[0].message)
// no migration path from "v3" to "v1".
// schemas reachable from "v3": flat;
// no schema has a migration path to "v1"
console.log(result.issues[0].meta)
// {
// reachableFromSource: ['flat'],
// reachableToTarget: []
// }
}The meta field gives you the raw arrays if you want to do something programmatic with them. The message has it all in a readable format.
The reachability info is computed on demand when an error occurs, not upfront. It adds no overhead to successful transforms.
Lifecycle hooks
Hooks let you observe what the registry is doing without touching migration code. They're good for logging, metrics, or just figuring out why something is slow.
onTransform
Called after every transform completes, whether it succeeded or failed.
const registry = createRegistry({
schemas: {
/* ... */
},
migrations: {
/* ... */
},
hooks: {
onTransform: (info) => {
console.log(
`${info.from} -> ${info.to} [${info.ok ? 'ok' : 'fail'}] ${info.durationMs.toFixed(1)}ms`,
)
},
},
})type TransformHookInfo = {
from: string
to: string
path: string[] | null // resolved path (null on early errors)
durationMs: number // wall clock time for the whole transform
ok: boolean // did it succeed?
}onStep
Called after each individual migration step. Useful for finding which step in a long chain is slow or failing.
hooks: {
onStep: (info) => {
const tag = info.label ? ` (${info.label})` : ''
console.log(` step ${info.index + 1}/${info.total}: ${info.from} -> ${info.to}${tag} ${info.durationMs.toFixed(1)}ms`)
},
}type StepHookInfo = {
from: string
to: string
index: number // zero-based step index
total: number // total number of steps
label?: string // from migration metadata
durationMs: number // wall clock time for this step
ok: boolean // did this step succeed?
}onWarning
Called when warnings are emitted from ctx.warn(), ctx.defaulted(), deprecated migration usage, or migration conflicts at construction time.
hooks: {
onWarning: (message, from, to) => {
console.log(`[${from}->${to}] ${message}`)
},
}Putting it together
Here's a minimal logging setup that gives you good visibility:
const registry = createRegistry({
schemas: {
/* ... */
},
migrations: {
/* ... */
},
hooks: {
onTransform: ({ from, to, ok, durationMs, path }) => {
const status = ok ? 'ok' : 'FAIL'
const route = path ? path.join(' -> ') : 'no path'
console.log(`[transform] ${from} -> ${to} [${status}] ${durationMs.toFixed(1)}ms (${route})`)
},
onStep: ({ from, to, index, total, label, durationMs }) => {
const tag = label ? ` "${label}"` : ''
console.log(
` [step ${index + 1}/${total}] ${from} -> ${to}${tag} ${durationMs.toFixed(1)}ms`,
)
},
onWarning: (msg, from, to) => {
console.log(` [warn] ${from} -> ${to}: ${msg}`)
},
},
})Output looks something like:
[warn] v1 -> v2: defaulted email: generated from name
[step 1/2] v1 -> v2 "add-email" 0.5ms
[step 2/2] v2 -> v3 "rename-fields" 0.0ms
[transform] v1 -> v3 [ok] 4.6ms (v1 -> v2 -> v3)