Registry
The core schema and migration container
The Registry is the central piece of doba. It holds your schemas and migration functions, and provides methods to transform data between them.
createRegistry()
Creates a new registry instance. The migration graph is pre-computed at construction time.
import { createRegistry } from 'dobajs'
import { z } from 'zod'
const registry = createRegistry({
schemas: {
v1: z.object({ name: z.string() }),
v2: z.object({ firstName: z.string(), lastName: z.string() }),
},
migrations: {
'v1->v2': (v1) => ({
firstName: v1.name.split(' ')[0],
lastName: v1.name.split(' ')[1] || '',
}),
},
})When identify is provided, the return type gains identify() and identifyAndTransform() methods. When omitted, those methods don't exist on the type.
function createRegistry<Schemas extends SchemaMap>(
config: RegistryConfig<Schemas> & { identify: IdentifyConfig<SchemaKeys<Schemas>> },
): Registry<Schemas, true>
function createRegistry<Schemas extends SchemaMap>(
config: RegistryConfig<Schemas>,
): Registry<Schemas, false>Registry
type Registry<
Schemas extends SchemaMap,
HasIdentify extends boolean = false,
> = RegistryBase<Schemas> & (HasIdentify extends true ? RegistryIdentify<Schemas> : unknown)RegistryBase contains the methods that are always present (transform, validate, has, hasMigration, findPath, explain, schemas). RegistryIdentify adds identify and identifyAndTransform when the identify config option is set. Both are internal types that get merged into the public Registry type.
RegistryConfig
type RegistryConfig<Schemas extends SchemaMap> = {
readonly schemas: Schemas
readonly migrations: MigrationsFor<Schemas>
readonly pathStrategy?: PathStrategy | undefined
readonly hooks?: RegistryHooks<SchemaKeys<Schemas>> | undefined
readonly debug?: boolean | undefined
readonly identify?: IdentifyConfig<SchemaKeys<Schemas>> | undefined
}| Option | Type | Default | Description |
|---|---|---|---|
schemas | SchemaMap | (required) | Map of schema names to Standard Schema compliant objects. |
migrations | MigrationsFor<Schemas> | (required) | Migration definitions. Keys use from->to or from<->to syntax. |
pathStrategy | 'shortest' | 'direct' | 'shortest' | How to find migration paths. |
hooks | RegistryHooks | Lifecycle hooks. See Debugging. | |
debug | boolean | false | Logs all hook activity to the console. |
identify | IdentifyConfig | Guard map or function for schema detection. See Identification. |
RegistryHooks
type RegistryHooks<Keys extends string = string> = {
readonly onWarning?: ((message: string, from: Keys, to: Keys) => void) | undefined
readonly onTransform?: ((info: TransformHookInfo<Keys>) => void) | undefined
readonly onStep?: ((info: StepHookInfo<Keys>) => void) | undefined
}| Hook | When it fires |
|---|---|
onWarning | ctx.warn(), ctx.defaulted(), deprecated migration usage, migration conflicts |
onTransform | After every transform completes (success or failure). Includes timing and path info. |
onStep | After each migration step completes. Includes step index, label, and timing. |
See Debugging for usage examples.
TransformHookInfo
type TransformHookInfo<Keys extends string = string> = {
readonly from: Keys
readonly to: Keys
readonly path: readonly Keys[] | null
readonly durationMs: number
readonly ok: boolean
}StepHookInfo
type StepHookInfo<Keys extends string = string> = {
readonly from: Keys
readonly to: Keys
readonly index: number
readonly total: number
readonly label?: string | undefined
readonly durationMs: number
readonly ok: boolean
}Properties
schemas
The registered schemas, exactly as passed to createRegistry.
registry.schemas // { v1: ZodObject<...>, v2: ZodObject<...> }Methods
transform()
Transforms data from one schema to another.
registry.transform<From, To>(
value: InferOutput<Schemas[From]>,
from: From,
to: To,
options?: TransformOptions<Keys>,
): Promise<TransformResult<InferOutput<Schemas[To]>, Keys, From, To>>Returns a TransformResult. The From and To type parameters are inferred from the arguments, which narrows the from/to fields throughout the result metadata.
const result = await registry.transform(data, 'v1', 'v2')
if (result.ok) {
result.value // typed as InferOutput<Schemas['v2']>
result.meta.path // readonly Keys[]
result.meta.steps // StepInfo with narrowed from/to
result.meta.warnings // WarningInfo with narrowed from/to
result.meta.defaults // DefaultedInfo with narrowed from/to
}TransformOptions
type TransformOptions<Keys extends string> = {
readonly path?: readonly Keys[] | undefined
readonly pathStrategy?: PathStrategy | undefined
readonly validate?: 'none' | 'end' | 'each' | undefined
readonly validatePath?: boolean | undefined
}| Option | Type | Default | Description |
|---|---|---|---|
validate | 'none' | 'end' | 'each' | 'end' | When to validate against schemas. |
path | readonly Keys[] | (auto) | Explicit migration path. Overrides automatic path finding. |
pathStrategy | 'shortest' | 'direct' | (registry) | Override the registry-level path strategy for this transform. |
validatePath | boolean | false | Check that every step has a migration before executing. |
validate()
Validates a value against a registered schema without transforming.
registry.validate<K>(
value: unknown,
schema: K,
): Promise<ValidateResult<InferOutput<Schemas[K]>, K>>Returns a ValidateResult with meta.schema set to the schema key.
const result = await registry.validate(data, 'v2')
if (result.ok) {
result.value // typed as InferOutput<Schemas['v2']>
result.meta.schema // 'v2'
}has()
Type guard to check if a schema name is registered.
registry.has<K>(schema: K): schema is K & SchemaKeys<Schemas>if (registry.has(name)) {
// name is narrowed to a valid schema key
}hasMigration()
Checks if a direct migration exists between two schemas. Does not consider multi-step paths.
registry.hasMigration(from: From, to: To): booleanregistry.hasMigration('v1', 'v2') // true
registry.hasMigration('v2', 'v1') // false (unless defined)findPath()
Returns the sequence of schemas needed to migrate from one to another, or null if no path exists.
registry.findPath(from: From, to: To): readonly Keys[] | nullconst path = registry.findPath('v1', 'v3')
// ['v1', 'v2', 'v3'] or nullUses the registry's pathStrategy to determine the algorithm (BFS or Dijkstra).
explain()
Returns a diagnostic description of the migration path between two schemas without running any migrations. Includes costs, labels, deprecation info, and a human-readable summary.
registry.explain<From, To>(
from: From,
to: To,
): ExplainResult<Keys, From, To>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]When no path exists, the summary includes which schemas are reachable from the source and which schemas can reach the target.
See Debugging for full details.
identify()
Detects which schema an unknown value belongs to. Only present when identify is configured.
registry.identify(value: unknown): Promise<IdentifyResult<Keys>>Runs configured guards synchronously first, then tries tryParse schemas in parallel if no sync guard matched.
const result = await registry.identify(unknownData)
if (result.ok) {
result.value // schema key, e.g. "database"
result.meta.schema // same as result.value
}Returns an IdentifyResult. Fails with identify_failed if no guard matches, or identify_ambiguous if multiple tryParse schemas validate.
identifyAndTransform()
Identifies the source schema and transforms to a target in one call. Only present when identify is configured.
registry.identifyAndTransform<To>(
value: unknown,
to: To,
options?: TransformOptions<Keys>,
): Promise<IdentifyTransformResult<InferOutput<Schemas[To]>, Keys>>Returns an IdentifyTransformResult with the same shape as TransformResult, plus meta.from indicating the detected source schema. Accepts the same TransformOptions as transform().
const result = await registry.identifyAndTransform(unknownData, 'frontend')
if (result.ok) {
result.value // typed as FrontendUser
result.meta.from // "database" (detected source)
result.meta.path // ["database", "frontend"]
result.meta.steps // per-step metadata
}See Schema Identification for the full guide.
Result types
TransformResult
type TransformResult<
T,
Keys extends string = string,
From extends string = Keys,
To extends string = Keys,
> = Result<T, readonly DobaIssue[], TransformMeta<Keys, From, To>>On success, carries the transformed value typed to the target schema output, plus TransformMeta. On failure, carries an array of DobaIssue.
TransformMeta
type TransformMeta<
Keys extends string = string,
From extends string = Keys,
To extends string = Keys,
> = {
readonly path: readonly Keys[]
readonly steps: readonly StepInfo<NarrowExclude<Keys, To>, NarrowExclude<Keys, From>>[]
readonly warnings: readonly WarningInfo<NarrowExclude<Keys, To>, NarrowExclude<Keys, From>>[]
readonly defaults: readonly DefaultedInfo<NarrowExclude<Keys, To>, NarrowExclude<Keys, From>>[]
}| Field | Type | Description |
|---|---|---|
path | readonly Keys[] | Ordered schema keys traversed, e.g. ['v1', 'v2', 'v3']. |
steps | readonly StepInfo[] | Per-step metadata for each migration executed. |
warnings | readonly WarningInfo[] | Warnings from ctx.warn() and deprecated migrations. |
defaults | readonly DefaultedInfo[] | Fields filled with default values via ctx.defaulted(). |
The From and To params flow through from TransformResult. The from/to fields on each step and warning are narrowed via NarrowExclude to exclude the endpoints of the overall transform.
StepInfo
type StepInfo<FromKey extends string = string, ToKey extends string = FromKey> = {
readonly from: FromKey
readonly to: ToKey
readonly label?: string | undefined
readonly deprecated?: string | boolean | undefined
}ExplainResult
type ExplainResult<
Keys extends string = string,
From extends string = Keys,
To extends string = Keys,
> = {
readonly from: From
readonly to: To
readonly path: readonly Keys[] | null
readonly totalCost: number | null
readonly steps: readonly ExplainStep<NarrowExclude<Keys, To>, NarrowExclude<Keys, From>>[]
readonly summary: string
}ExplainStep
type ExplainStep<FromKey extends string = string, ToKey extends string = FromKey> = {
readonly from: FromKey
readonly to: ToKey
readonly cost: number
readonly label?: string | undefined
readonly deprecated?: string | boolean | undefined
}ValidateResult
type ValidateResult<T, Key extends string = string> = Result<
T,
readonly DobaIssue[],
ValidateMeta<Key>
>ValidateMeta
type ValidateMeta<Key extends string = string> = {
readonly schema: Key
}Utility types
NarrowExclude
type NarrowExclude<T extends string, U extends string> =
[Exclude<T, U>] extends [never] ? T : Exclude<T, U>Narrows T by excluding U, falling back to T when the result would be never. Used throughout TransformMeta, StepInfo, WarningInfo, DefaultedInfo, and ExplainResult to narrow from/to fields on steps and warnings.
For example, when you call transform(data, 'v1', 'v3') on a registry with keys 'v1' | 'v2' | 'v3':
step.fromis typed asNarrowExclude<Keys, 'v3'>='v1' | 'v2'(the target can't appear as a step source)step.tois typed asNarrowExclude<Keys, 'v1'>='v2' | 'v3'(the source can't appear as a step target)
PathStrategy
type PathStrategy = 'direct' | 'shortest'| Value | Algorithm | Description |
|---|---|---|
'shortest' | BFS/Dijkstra | Finds the optimal path through the migration graph. Default. |
'direct' | Lookup | Only uses a direct migration between two schemas. |