Files
dbtype/docs/architecture/module.md
glm-5.1 98764086f4 docs: resolve all open architecture questions with ADRs 009-014
Resolve OQ-01 through OQ-10 with formal ADRs and update all architecture
docs to reference decisions. Add two new tracked questions (OQ-11, OQ-12)
surfaced during review.

ADR-009: Keep FooRelations naming convention for relation entries
ADR-010: One module per database, include derived schemas by default
ADR-011: Support JSX/TSX as ergonomic authoring layer
ADR-012: Always .returning() with graceful fallback per dialect
ADR-013: Adapter generates relations from module entries (no <relation> element)
ADR-014: Leverage drizzle-kit for migrations, no native migration generator

Also upgrades elements.md, hosts.md, repo-adapter.md status to stable,
clarifies OQ-06 as design clarification, and specifies MySQL .returning()
detection mechanism in ADR-012.
2026-05-23 12:47:55 +00:00

7.0 KiB

status, last_updated
status last_updated
stable 2026-05-23

Module: Type.Module Mechanical Reference

How Type.Module works mechanically — construction, validation, serialization, migration diffing, cross-module references, and constraints. For the domain-specific content (what goes into a dbtype module, schema derivation semantics, relations), see schema.md.

Construction Patterns

Basic Pattern

const defs: Record<string, any> = {
  Users: Type.Object({ id: Type.String({ format: 'uuid' }), name: Type.String() }),
  Tasks: Type.Object({ id: Type.String({ format: 'uuid' }), title: Type.String() }),
}

const M = Type.Module(defs)
const Users = M.Import('Users')

Incremental Construction

The defs map is a plain Record<string, TSchema> — it can be built incrementally, mutated, and extended before compilation:

const defs: Record<string, any> = {}
defs.Users = extractTableSchema(UsersElement)
defs.Tasks = extractTableSchema(TasksElement)

// Add a column later
defs.Users = Type.Object({ ...defs.Users.properties, role: Type.String() })

// Add relations
defs.UsersRelations = Type.Object({ tasks: Type.Array(Type.Ref('Tasks')) })

// Compile when ready
const M = Type.Module(defs)

Once compiled, mutations to the original defs map don't affect the compiled module.

With Relations

defs.UsersRelations = Type.Object({ tasks: Type.Array(Type.Ref('Tasks')) })
defs.TasksRelations = Type.Object({ user: Type.Ref('Users') })

Type.Ref('Users') within the module resolves to the Users schema. No circular import issues.

With Derived Schemas

defs.InsertUsers = Type.Object({ name: Type.String(), email: Type.String() })  // manual
defs.UpdateUsers = Type.Partial(Type.Ref('Users'))  // computed

Validation

Format Registration Required

TypeBox treats format as an annotation by default. To enforce format validation, register custom formats:

import { FormatRegistry } from '@alkdev/typebox'

FormatRegistry.Set('uuid', (value) => /^[0-9a-f]{8}-...$/i.test(value))
FormatRegistry.Set('email', (value) => /^[^@]+@[^@]+\.[^@]+$/.test(value))

After registration, Value.Check enforces these formats.

Validation Pattern

const M = Type.Module(defs)
const Users = M.Import('Users')

// Valid
Value.Check(Users, { id: '550e8400-e29b-41d4-a716-446655440000', name: 'alice', email: 'a@b.com', ... })

// Invalid — Value.Check returns false, Value.Errors provides details
Value.Check(Users, { id: 'bad-uuid', ... })  // false
for (const err of Value.Errors(Users, badData)) { ... }

Serialization

JSON.stringify(M.Import(key)) produces JSON Schema with $defs:

{
  "$defs": {
    "Users": { "$id": "Users", "type": "object", "properties": { ... }, "required": [...] },
    "Tasks": { "$id": "Tasks", "type": "object", "properties": { ... }, "required": [...] },
    "UsersRelations": { "$id": "UsersRelations", "type": "object", "properties": { "tasks": { "type": "array", "items": { "$ref": "Tasks" } } } }
  },
  "$ref": "Users"
}

Key properties:

  • Each $defs entry has an $id matching its key
  • Type.Ref remains as { "$ref": "Key" } — not inlined; consumers must resolve them
  • The entire structure is valid JSON Schema
  • All entries in the module are present in $defs (even if only one was imported)

Roundtrip

The serialized form can be parsed back into a schema-like structure. Value.Diff works on these serialized objects to produce structural edit lists. Note that Symbol properties ([Kind], [Hint], etc.) are stripped by JSON.stringify — the serialized form is JSON Schema, not TypeBox schema. Roundtripping requires FromSchema or reconstructed TypeBox objects.

Migration Diffing

const v1 = JSON.parse(JSON.stringify(M.Import('Users')))

// Modify schema
defs.Users = Type.Object({ ...defs.Users.properties, role: Type.String() })

const M2 = Type.Module(defs)
const v2 = JSON.parse(JSON.stringify(M2.Import('Users')))

const edits = Value.Diff(v1, v2)
// [
//   { type: 'insert', path: '/$defs/Users/properties/role', value: { type: 'string' } },
//   { type: 'update', path: '/$defs/Users/required/3', value: 'role' },
// ]

Edits use JSON Pointer paths. A migration generator can translate these to:

  • INSERT for new properties → ALTER TABLE ADD COLUMN
  • DELETE for removed properties → ALTER TABLE DROP COLUMN
  • UPDATE for type changes → ALTER TABLE ALTER COLUMN TYPE

This is structural diffing, not semantic — it doesn't understand that changing Type.String() to Type.String({ maxLength: 255 }) is a constraint addition, not a type change. Semantic diffing is a future concern.

Migration generation: Leverage drizzle-kit for migrations. Since dbtype's primary output is Drizzle table definitions, drizzle-kit generate handles migration generation from those tables. The module's Value.Diff capability remains available as a foundation for future migration tooling, but is not used for production migrations in phase 1 or the foreseeable future. See ADR-014.

Cross-Module References

Module.Import() embeds the source module's $defs in the resulting TImport schema. This enables referencing types from another module:

const CommonM = Type.Module({ Uuid: Type.String({ format: 'uuid' }) })
const CommonUuid = CommonM.Import('Uuid')

const AppM = Type.Module({
  User: Type.Object({ id: CommonUuid, name: Type.String() }),
})

However, this nests $defs within $defs (the User's $defs contains CommonUuid's $defs), which increases payload size. For dbtype's use case, keeping everything in a single module is simpler and avoids nesting.

Constraints

  • Module entries are computed at construction timeType.Partial(Type.Ref('Users')) is resolved when the module is built, producing a concrete optional-property object
  • Type.Ref outside a module has static: unknown — always use M.Import(key) for proper type inference
  • Module keys are a flat namespace — no nested paths like "tables/Users". Table names must be unique within the module.
  • Type.Ref resolves within the module only — no cross-module references without Module.Import
  • Module.Import embeds all $defs — every import carries the full module. This is correct for validation but increases JSON Schema size.
  • Defs map is mutable until compiled — once passed to Type.Module, mutations to the original map don't affect the compiled module
  • Format validation requires FormatRegistry.Setuuid, email, and other custom formats must be registered before Value.Check will enforce them
  • Symbol properties are lost in JSON.stringify[Kind], [Hint], etc. are stripped. The serialized form is JSON Schema, not TypeBox schema. Roundtripping requires FromSchema or reconstructed TypeBox objects.

References

  • TypeBox Module: @alkdev/typebox/src/type/module/module.ts
  • TypeBox Format: @alkdev/typebox/src/type/registry/format.ts
  • Probe: scripts/probe-e2e.ts