Add SDD architecture docs for dbtype
Phase 0 architecture specification following the alkdev documentation pattern from @alkdev/flowgraph. Documents the validated architecture (UJSX elements → Type.Module → Drizzle hosts) based on e2e probe results. Docs added: - README: Project overview, architecture, current state - architecture/README: Index, design decisions, relationships - architecture/schema: Type.Module as bundle, construction, serialization - architecture/hosts: HostConfig per dialect, column mapping, symbolic defaults - architecture/elements: UJSX element types, props, function components - architecture/module: Module mechanics, format registration, diffing - architecture/repo-adapter: from-dbtype operations adapter (phase 2) - architecture/build-distribution: Package structure, exports - architecture/open-questions: 10 open questions across all topics - ADRs 001-005: UJSX as IR, Type.Module, HostConfig, format, repo adapter
This commit is contained in:
170
docs/architecture/module.md
Normal file
170
docs/architecture/module.md
Normal file
@@ -0,0 +1,170 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-05-22
|
||||
---
|
||||
|
||||
# Module: Type.Module as the Schema Bundle
|
||||
|
||||
Technical details on how dbtype uses `Type.Module` for schema construction, validation, serialization, and migration.
|
||||
|
||||
## Overview
|
||||
|
||||
dbtype uses `@alkdev/typebox`'s `Type.Module` as the schema storage and resolution mechanism. A module holds all table schemas, their relations, and derived schemas (insert, update, partial) in a single flat namespace. `Type.Ref` resolves cross-table references — including circular ones — without import ordering issues.
|
||||
|
||||
This document covers the mechanics, constraints, and patterns discovered during architecture probing.
|
||||
|
||||
## Construction Patterns
|
||||
|
||||
### Basic Pattern
|
||||
|
||||
```typescript
|
||||
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')
|
||||
```
|
||||
|
||||
### With Relations
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
defs.InsertUsers = Type.Object({ name: Type.String(), email: Type.String() }) // manual
|
||||
defs.UpdateUsers = Type.Partial(Type.Ref('Users')) // computed
|
||||
```
|
||||
|
||||
### Incremental Construction
|
||||
|
||||
```typescript
|
||||
// Build defs incrementally
|
||||
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)
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
### Format Registration Required
|
||||
|
||||
TypeBox treats `format` as an annotation by default. To enforce format validation, register custom formats:
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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`:
|
||||
|
||||
```json
|
||||
{
|
||||
"$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
|
||||
- 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.
|
||||
|
||||
## Migration Diffing
|
||||
|
||||
```typescript
|
||||
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.
|
||||
|
||||
## Cross-Module References
|
||||
|
||||
`Module.Import()` embeds the source module's `$defs` in the resulting `TImport` schema. This enables referencing types from another module:
|
||||
|
||||
```typescript
|
||||
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 time** — `Type.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.
|
||||
- **`Module.Import` embeds all `$defs`** — every import carries the full module. This is correct for validation but increases JSON Schema size.
|
||||
- **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`
|
||||
Reference in New Issue
Block a user